import { ServiceFare, ServiceClass, Service, FareType, Paging } from "entity"; import { NextFunction, Response } from "express"; import { Request } from "express-jwt"; import Joi from "joi"; import { ILogObj, Logger } from "tslog"; import CommonHelper from "../helpers/common"; import { ReturnHelper } from "../helpers/express/return"; import { Language } from "../langs/lang"; import { ServiceFareModel } from "../model/service_fare"; import { ServiceClassModel } from "../model/service_class"; import { ServiceModel } from "../model/service"; import { OrmHelper } from "../helpers/orm"; import moment from "moment"; const log: Logger = new Logger({ name: "[ServiceFareController]", type: "pretty", }); export class ServiceFareController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['filter'] = { description: 'Filter data in JSON format. Available fields: {\"any\": \"value\"} for searching by Service.name or service type name (case-insensitive, supports partial match)', in: 'query', type: 'string', example: '{\"any\": \"express\"}' } #swagger.parameters['limit'] = { in: 'query', required: true, type: 'number' } #swagger.parameters['page'] = { in: 'query', required: true, type: 'number' } #swagger.parameters['with_deleted'] = { in: 'query', required: true, type: 'boolean' } #swagger.parameters['order_field'] = { in: 'query', required: true, type: 'string' } #swagger.parameters['order_direction'] = { in: 'query', required: true, schema: { '@enum': ['ASC', 'DESC'] } } */ try { const schema = Joi.object().keys({ filter: Joi.string().allow("").optional().label("Filter"), page: Joi.number().required().min(1).label("Page"), limit: Joi.number().required().min(1).label("Limit"), with_deleted: Joi.bool().required().label("With Deleted"), order_field: Joi.string().required().label("Order Field"), order_direction: Joi.string().valid("ASC", "DESC").uppercase().required().label("Order Direction"), }); let param: Paging = await schema.validateAsync(req.query); let filter = param.filter && param.filter !== "" ? JSON.parse(param.filter) : {}; const offset = (param.page - 1) * param.limit; const limit = param.limit; const filterAny = filter.any && filter.any !== "" ? String(filter.any) : ""; let filterObj = { ...filter } as any; delete filterObj.any; var query = await ServiceModel.list(filterObj); const base_query = query .select([ "Service.id", "Service.name", ]) .leftJoin("Service.serviceType", "st") .addSelect("st.name", "service_type_name") .leftJoin( qb => { return qb .select('sf."serviceId"', "serviceId") .addSelect("COALESCE(MIN(sf.fare), 0)", "min") .addSelect("COALESCE(MAX(sf.fare), 0)", "max") .from("service_fares", "sf") .innerJoin("fare_type", "ft", 'sf."faretypeId" = ft.id AND ft.deleted_at IS NULL') .innerJoin("service_classes", "sc", 'sf."serviceClassId" = sc.id AND sc.deleted_at IS NULL') .innerJoin("services", "s", 'sf."serviceId" = s.id AND s.deleted_at IS NULL') .groupBy('sf."serviceId"'); }, "sfa", 'sfa."serviceId" = Service.id' ) .addSelect('COALESCE(sfa.min, 0)', "min_fare") .addSelect('COALESCE(sfa.max, 0)', "max_fare"); const res_count = base_query.clone(); const res_list = base_query .orderBy("Service." + param.order_field, param.order_direction) .skip(offset) .take(limit); if (filterAny !== "") { const anyVal = filterAny.includes("%") ? filterAny : `%${filterAny}%`; res_list.andWhere("(Service.name ILIKE :any OR st.name ILIKE :any)", { any: anyVal }); res_count.andWhere("(Service.name ILIKE :any OR st.name ILIKE :any)", { any: anyVal }); } if (param.with_deleted) { res_count.withDeleted(); res_list.withDeleted(); } const current_page = param.page; const total_count_data = await res_count.getCount(); const list_data = await res_list.getRawAndEntities(); const merged = list_data.entities.map((entity, index) => ({ ...entity, min_fare: list_data.raw[index]?.min_fare ?? null, max_fare: list_data.raw[index]?.max_fare ?? null, service_type_name: list_data.raw[index]?.service_type_name ?? null, })); const count_data = CommonHelper.countObject(merged); return ReturnHelper.successResponselist(res, 200, Language.lang.success_view, count_data, current_page, total_count_data, merged); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); } } static async option(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] */ try { const [services, fareTypes, serviceClasses] = await Promise.all([ OrmHelper.DB.getRepository(Service) .createQueryBuilder("Service") .select(["Service.id", "Service.name"]) .getMany(), OrmHelper.DB.getRepository(FareType) .createQueryBuilder("FareType") .select(["FareType.id", "FareType.name"]) .getMany(), OrmHelper.DB.getRepository(ServiceClass) .createQueryBuilder("ServiceClass") .select(["ServiceClass.id", "ServiceClass.name"]) .getMany(), ]); // Semua FareType dapat semua ServiceClass const fareTypesWithClass = fareTypes.map((ft) => ({ id: ft.id, name: ft.name, service_class: serviceClasses.map((sc) => ({ id: sc.id, name: sc.name, })), })); // Semua Service dapat semua FareType (yang sudah berisi ServiceClass) const list_data = services.map((svc) => ({ service: { id: svc.id, name: svc.name, faretype: fareTypesWithClass, }, })); const total_count_data = list_data.length; const count_data = CommonHelper.countObject(list_data); return ReturnHelper.successResponselist( res, 200, Language.lang.success_view, count_data, 1, total_count_data, list_data ); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); } } static async create(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/serviceFare" } } } } */ const queryRunner = OrmHelper.DB.createQueryRunner(); await queryRunner.startTransaction(); try { const schema = Joi.object().keys({ service_id: Joi.string().uuid().required().label("Service ID"), faretype: Joi.array().items( Joi.object().keys({ faretype_id: Joi.string().uuid().required().label("Fare Type ID"), service_class: Joi.array().items( Joi.object().keys({ service_class_id: Joi.string().uuid().required().label("Service Class ID"), fare: Joi.number().min(0).allow(null).label("Fare"), }) ).min(1).required().label("Service Class"), }) ).min(1).required().label("Fare Type"), }); let param: any = await schema.validateAsync(req.body); // Validasi Service let service = await ServiceModel.list({ "Service.id": param.service_id }).then((q) => q.getOne()); if (!service) throw { message: "Service not found" }; const savedServiceFares: ServiceFare[] = []; // Loop tiap faretype for (const ft of param.faretype) { // Validasi FareType let fareType = await OrmHelper.DB.getRepository("FareType") .createQueryBuilder("FareType") .where("FareType.id = :id", { id: ft.faretype_id }) .getOne(); if (!fareType) throw { message: `Fare Type ID ${ft.faretype_id} not found` }; for (const sc of ft.service_class) { let serviceClass = await ServiceClassModel.list({ "ServiceClass.id": sc.service_class_id }).then((q) => q.getOne()); if (!serviceClass) throw { message: `Service Class ID ${sc.service_class_id} not found` }; let serviceFare = await ServiceFareModel.list({ "ServiceFare.serviceId": param.service_id, "ServiceFare.faretype": ft.faretype_id, "ServiceFare.serviceClassId": sc.service_class_id, }).then((q) => q.getOne()); if (!serviceFare) { serviceFare = new ServiceFare(); serviceFare.service = service; serviceFare.faretype = ft.faretype_id; serviceFare.serviceClass = serviceClass; serviceFare.created_by = req.auth?.data.name; } serviceFare.fare = String(sc.fare ?? 0); serviceFare.updated_by = req.auth?.data.name; const saved = await queryRunner.manager.save(serviceFare); savedServiceFares.push(saved); } } await queryRunner.commitTransaction(); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, savedServiceFares); } catch (e: unknown) { await queryRunner.rollbackTransaction(); log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); } finally { await queryRunner.release(); } } static async detail(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['id'] = { description: 'Service ID', in: 'path', type: 'string' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("Service Id"), // ← label diupdate }); let param: any = await schema.validateAsync(req.params); let serviceFare = await ServiceModel.list({ "Service.id": param.id }) .then((q) => q .select([ "Service.id", "Service.name", "servicefare.id", "servicefare.fare", "serviceClass.id", "serviceClass.name", "faretype.id", "faretype.name", ]) .leftJoin("Service.servicefare", "servicefare") .leftJoin("servicefare.serviceClass", "serviceClass") .leftJoin("servicefare.faretype", "faretype") .getOne() ); if (!serviceFare || serviceFare.length === 0) throw { message: "Service Fare Not Found" }; return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, serviceFare); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_not_found, err.message); } } static async update(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['id'] = { description: 'Service Fare ID', in: 'path', type: 'string' } #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/serviceFareUpdate" } } } } */ const queryRunner = OrmHelper.DB.createQueryRunner(); await queryRunner.startTransaction(); try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("Service Fare ID"), fare: Joi.number().min(0).required().label("Fare"), }); let param: any = await schema.validateAsync({ ...req.body, id: req.params.id, }); // Cari data berdasarkan ID let serviceFare = await ServiceFareModel.list({ "ServiceFare.id": param.id }).then((q) => q.getOne()); if (!serviceFare) throw { message: "Service Fare " + Language.lang.failed_not_found }; // Update fare saja serviceFare.fare = String(param.fare); serviceFare.updated_by = req.auth?.data.name; await queryRunner.manager.save(serviceFare); await queryRunner.commitTransaction(); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, serviceFare); } catch (e: unknown) { await queryRunner.rollbackTransaction(); log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message); } finally { await queryRunner.release(); } } static async delete(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } #swagger.parameters['hard'] = { in: 'path', description: 'Is Hard Delete', required: false, type: 'boolean' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), hard: Joi.bool().optional().allow("").label("Is hard delete?"), }); const param: { id: string; hard: boolean } = await schema.validateAsync(req.params); let affected = 0; if (!param.hard) { let serviceFare: ServiceFare = await ServiceFareModel.list({ "ServiceFare.id": param.id }).then((q) => q.getOne()); if (!serviceFare) throw { message: "Service Fare " + Language.lang.failed_not_found }; serviceFare.deleted_by = req.auth?.data.name; serviceFare.deleted_at = moment().toDate(); await OrmHelper.DB.manager.save(serviceFare); affected = 1; } else { const repo = OrmHelper.DB.getRepository(ServiceFare); affected = (await repo.delete({ id: param.id })).affected ?? 0; } if (affected > 0) { return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); } } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message); } } static async restore(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Service Fare'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['id'] = { in: 'path', required: true, type: 'string' } */ try { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("ID"), }); const param: any = await schema.validateAsync(req.params); const repo = OrmHelper.DB.getRepository(ServiceFare); const affected = (await repo.restore({ id: param.id })).affected ?? 0; if (affected > 0) { return ReturnHelper.successResponseAny(res, 200, Language.lang.success_restore, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); } } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_restore, err.message); } } }