diff --git a/Dockerfile b/Dockerfile index 0b4ded3..9b354f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,11 @@ COPY . . RUN yarn install RUN yarn build FROM node:18.17-alpine -# ARG env_target +ARG env_target WORKDIR /app -# RUN echo ${env_target} -# COPY env/$env_target /app/.env -# COPY --from=builder /app/env/$env_target .env +RUN echo ${env_target} +COPY env/$env_target /app/.env +COPY --from=builder /app/env/$env_target .env COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/assets ./assets diff --git a/assets/email-template/redesign/change-date-information.html b/assets/email-template/redesign/change-date-information.html new file mode 100644 index 0000000..586aabb --- /dev/null +++ b/assets/email-template/redesign/change-date-information.html @@ -0,0 +1,2027 @@ + + + + + Change Date Information + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/invoice-bank.html b/assets/email-template/redesign/invoice-bank.html new file mode 100644 index 0000000..858a633 --- /dev/null +++ b/assets/email-template/redesign/invoice-bank.html @@ -0,0 +1,2119 @@ + + + + + Invoice Bank Transfer + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/invoice-expired.html b/assets/email-template/redesign/invoice-expired.html new file mode 100644 index 0000000..88488fa --- /dev/null +++ b/assets/email-template/redesign/invoice-expired.html @@ -0,0 +1,1962 @@ + + + + + Invoice Expired + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/invoice-midtrans.html b/assets/email-template/redesign/invoice-midtrans.html new file mode 100644 index 0000000..2354e12 --- /dev/null +++ b/assets/email-template/redesign/invoice-midtrans.html @@ -0,0 +1,2037 @@ + + + + + Invoice Midtrans + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/payment-confirmation.html b/assets/email-template/redesign/payment-confirmation.html new file mode 100644 index 0000000..bdde960 --- /dev/null +++ b/assets/email-template/redesign/payment-confirmation.html @@ -0,0 +1,2454 @@ + + + + + Payment Confirmation + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/refund-confirmation.html b/assets/email-template/redesign/refund-confirmation.html new file mode 100644 index 0000000..03a6ce5 --- /dev/null +++ b/assets/email-template/redesign/refund-confirmation.html @@ -0,0 +1,2832 @@ + + + + + Refund Confirmation + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/email-template/redesign/refund-request.html b/assets/email-template/redesign/refund-request.html new file mode 100644 index 0000000..579340f --- /dev/null +++ b/assets/email-template/redesign/refund-request.html @@ -0,0 +1,2414 @@ + + + + + Refund Confirmation + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+ + diff --git a/assets/image/we.png b/assets/image/we.png new file mode 100644 index 0000000..cb44d28 Binary files /dev/null and b/assets/image/we.png differ diff --git a/src/app.module.ts b/src/app.module.ts index 2c9e87e..e552023 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -45,8 +45,10 @@ import { GoogleCalendarModule } from './modules/configuration/google-calendar/go import { TransactionModule } from './modules/transaction/transaction/transaction.module'; import { TransactionModel } from './modules/transaction/transaction/data/models/transaction.model'; import { + TransactionBreakdownTaxModel, TransactionItemBreakdownModel, TransactionItemModel, + TransactionItemTaxModel, } from './modules/transaction/transaction/data/models/transaction-item.model'; import { TransactionTaxModel } from './modules/transaction/transaction/data/models/transaction-tax.model'; import { ReconciliationModule } from './modules/transaction/reconciliation/reconciliation.module'; @@ -76,6 +78,10 @@ import { PosLogModel } from './modules/configuration/log/data/models/pos-log.mod import { ExportModule } from './modules/configuration/export/export.module'; import { TransactionDemographyModel } from './modules/transaction/transaction/data/models/transaction-demography.model'; import { SupersetModule } from './modules/configuration/superset/superset.module'; +import { GateScanModule } from './modules/gates/gate.module'; +import { UserLoginModel } from './modules/user-related/user/data/models/user-login.model'; +import { LogUserLoginModel } from './modules/configuration/log/data/models/log-user-login.model'; +import { AuthService } from './core/guards/domain/services/auth.service'; @Module({ imports: [ @@ -101,6 +107,7 @@ import { SupersetModule } from './modules/configuration/superset/superset.module ItemCategoryModel, ItemRateModel, LogModel, + LogUserLoginModel, NewsModel, PaymentMethodModel, PosLogModel, @@ -116,7 +123,11 @@ import { SupersetModule } from './modules/configuration/superset/superset.module TransactionTaxModel, TransactionDemographyModel, TransactionItemBreakdownModel, + TransactionItemTaxModel, + TransactionBreakdownTaxModel, UserModel, + UserLoginModel, + VipCategoryModel, VipCodeModel, @@ -178,9 +189,12 @@ import { SupersetModule } from './modules/configuration/superset/superset.module // superset SupersetModule, + + GateScanModule, ], controllers: [], providers: [ + AuthService, PrivilegeService, /** * By default all request from client will protect by JWT diff --git a/src/core/guards/domain/jwt.guard.ts b/src/core/guards/domain/jwt.guard.ts index d02fba3..a0d750f 100644 --- a/src/core/guards/domain/jwt.guard.ts +++ b/src/core/guards/domain/jwt.guard.ts @@ -7,10 +7,10 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { Observable } from 'rxjs'; import { SessionService, UsersSession } from 'src/core/sessions'; import { UNPROTECTED_URL } from '../constants'; import { PrivilegeService } from './services/privilege.service'; +import { AuthService } from './services/auth.service'; @Injectable({ scope: Scope.REQUEST }) export class JWTGuard implements CanActivate { @@ -18,14 +18,13 @@ export class JWTGuard implements CanActivate { protected readonly session: SessionService, protected readonly reflector: Reflector, protected readonly privilege: PrivilegeService, + protected readonly authService: AuthService, ) {} protected isPublic = false; protected userSession: UsersSession; - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { + async canActivate(context: ExecutionContext) { /** * Check if access url is protected or not * By default `isUnprotected` equals `false` @@ -61,9 +60,29 @@ export class JWTGuard implements CanActivate { */ try { this.userSession = this.session.verifyToken(token); + await this.authService.verifyRegisteredLoginToken(token); + Logger.log(`Access from ${this.userSession.name}`, 'AuthGuard'); return true; } catch (error) { + const expiredError = error.message; + if (expiredError === 'jwt expired') { + const [, body] = token.split('.'); + const bodyToken = JSON.parse(atob(body)); + + const user = { + role: bodyToken.role, + user_id: bodyToken.id, + username: bodyToken.username, + user_privilege_id: bodyToken.user_privilege_id, + item_id: bodyToken.item_id, + item_name: bodyToken.item_name, + source: bodyToken.source, + }; + + this.authService.logoutUser(user, token); + } + throw new UnauthorizedException({ code: 10001, message: diff --git a/src/core/guards/domain/roles.guard.ts b/src/core/guards/domain/roles.guard.ts index f52fa79..595eb41 100644 --- a/src/core/guards/domain/roles.guard.ts +++ b/src/core/guards/domain/roles.guard.ts @@ -9,7 +9,7 @@ import { MAIN_MENU } from '../constants'; @Injectable() export class RolesGuard extends JWTGuard { async canActivate(context: ExecutionContext): Promise { - super.canActivate(context); + await super.canActivate(context); // jika endpoint tersebut bukan public, maka lakukan check lanjutan if (!this.isPublic) { diff --git a/src/core/guards/domain/services/auth.service.ts b/src/core/guards/domain/services/auth.service.ts new file mode 100644 index 0000000..e5b76d6 --- /dev/null +++ b/src/core/guards/domain/services/auth.service.ts @@ -0,0 +1,78 @@ +import { + HttpStatus, + Injectable, + Scope, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { + CONNECTION_NAME, + OPERATION, +} from 'src/core/strings/constants/base.constants'; +import { DataSource } from 'typeorm'; +import { UserRole } from 'src/modules/user-related/user/constants'; +import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; +import { AppSource, LogUserType } from 'src/core/helpers/constant'; +import { EventBus } from '@nestjs/cqrs'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; +import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-login.model'; + +interface UserEntity { + user_id: string; + username: string; + role: UserRole; + user_privilege_id: string; + item_id: string; + item_name: string; + source: AppSource; +} + +@Injectable({ scope: Scope.REQUEST }) +export class AuthService { + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + protected readonly dataSource: DataSource, + + private eventBus: EventBus, + ) {} + + get repository() { + return this.dataSource.getRepository(UserLoginModel); + } + + async logoutUser(user: UserEntity, token: string) { + await this.repository.delete({ login_token: token }); + + const userLogout = { + type: LogUserType.logout, + created_at: new Date().getTime(), + name: user.username, + user_privilege_id: user.user_privilege_id, + ...user, + }; + + this.eventBus.publish( + new LogUserLoginEvent({ + id: user.user_id, + old: null, + data: userLogout, + user: userLogout as any, + description: 'Logout', + module: UserModel.name, + op: OPERATION.UPDATE, + }), + ); + } + + async verifyRegisteredLoginToken(token: string) { + const data = await this.repository.findOneBy({ login_token: token }); + + if (!data) { + throw new UnauthorizedException({ + statusCode: HttpStatus.UNAUTHORIZED, + message: `Invalid token`, + error: 'Unauthorized', + }); + } + } +} diff --git a/src/core/helpers/constant/index.ts b/src/core/helpers/constant/index.ts new file mode 100644 index 0000000..4da6b95 --- /dev/null +++ b/src/core/helpers/constant/index.ts @@ -0,0 +1,11 @@ +export enum LogUserType { + login = 'login', + logout = 'logout', +} + +export enum AppSource { + POS_ADMIN = 'POS_ADMIN', + POS_COUNTER = 'POS_COUNTER', + QUEUE_ADMIN = 'QUEUE_ADMIN', + QUEUE_CUSTOMER = 'QUEUE_CUSTOMER', +} diff --git a/src/core/response/constants.ts b/src/core/response/constants.ts index 1da092d..ae59a5a 100644 --- a/src/core/response/constants.ts +++ b/src/core/response/constants.ts @@ -1 +1,2 @@ export const PAGINATION_RESPONSE = 'PAGINATION_RESPONSE'; +export const GATE_RESPONSE = 'GATE_RESPONSE'; diff --git a/src/core/response/domain/decorators/pagination.response.ts b/src/core/response/domain/decorators/pagination.response.ts index 482db0c..b9ba684 100644 --- a/src/core/response/domain/decorators/pagination.response.ts +++ b/src/core/response/domain/decorators/pagination.response.ts @@ -1,5 +1,5 @@ import { SetMetadata } from '@nestjs/common'; -import { PAGINATION_RESPONSE } from '../../constants'; +import { GATE_RESPONSE, PAGINATION_RESPONSE } from '../../constants'; /** * This decorator will tell the response, @@ -7,3 +7,5 @@ import { PAGINATION_RESPONSE } from '../../constants'; */ export const Pagination = (isPagination = true) => SetMetadata(PAGINATION_RESPONSE, isPagination); + +export const Gate = () => SetMetadata(GATE_RESPONSE, true); diff --git a/src/core/response/domain/response.interceptor.ts b/src/core/response/domain/response.interceptor.ts index f9893d4..9f4d8de 100644 --- a/src/core/response/domain/response.interceptor.ts +++ b/src/core/response/domain/response.interceptor.ts @@ -8,13 +8,20 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; -import { PAGINATION_RESPONSE } from '../constants'; +import { GATE_RESPONSE, PAGINATION_RESPONSE } from '../constants'; import { createPaginationResponse } from './utils/pagination-meta.helper'; @Injectable() export class TransformInterceptor implements NestInterceptor { constructor(protected readonly reflector: Reflector) {} intercept(context: ExecutionContext, next: CallHandler): Observable { + const isGate = this.reflector.getAllAndOverride(GATE_RESPONSE, [ + context.getHandler(), + context.getClass(), + ]); + + if (isGate) return next.handle(); + const isPagination = this.reflector.getAllAndOverride( PAGINATION_RESPONSE, [context.getHandler(), context.getClass()], diff --git a/src/core/sessions/domain/entities/user-sessions.interface.ts b/src/core/sessions/domain/entities/user-sessions.interface.ts index 58004d5..848d44e 100644 --- a/src/core/sessions/domain/entities/user-sessions.interface.ts +++ b/src/core/sessions/domain/entities/user-sessions.interface.ts @@ -1,8 +1,10 @@ +import { AppSource } from 'src/core/helpers/constant'; import { UserRole } from 'src/modules/user-related/user/constants'; export interface UsersSession { id: number; name: string; role: UserRole; + source?: AppSource; user_privilege_id: string; } diff --git a/src/core/sessions/domain/providers/user.ts b/src/core/sessions/domain/providers/user.ts index 0c1b980..2787470 100644 --- a/src/core/sessions/domain/providers/user.ts +++ b/src/core/sessions/domain/providers/user.ts @@ -23,4 +23,9 @@ export class UserProvider { const [, token] = this.request.headers['authorization'].split(' '); return this.session.verifyToken(token); } + + get token(): string { + const [, token] = this.request.headers['authorization'].split(' '); + return token; + } } diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index 9fa6af6..990c4cb 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -22,8 +22,12 @@ export enum TABLE_NAME { TRANSACTION_ITEM = 'transaction_items', TRANSACTION_ITEM_BREAKDOWN = 'transaction_item_breakdowns', TRANSACTION_TAX = 'transaction_taxes', + TRANSACTION_ITEM_TAX = 'transaction_item_taxes', + TRANSACTION_ITEM_BREAKDOWN_TAX = 't_breakdown_item_taxes', TRANSACTION_DEMOGRAPHY = 'transaction_demographies', USER = 'users', + USER_LOGIN = 'users_login', + LOG_USER_LOGIN = 'log_users_login', USER_PRIVILEGE = 'user_privileges', USER_PRIVILEGE_CONFIGURATION = 'user_privilege_configurations', VIP_CATEGORY = 'vip_categories', diff --git a/src/database/migrations/1724926316235-add-value-variable-formula.ts b/src/database/migrations/1724926316235-add-value-variable-formula.ts new file mode 100644 index 0000000..b10b372 --- /dev/null +++ b/src/database/migrations/1724926316235-add-value-variable-formula.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddValueVariableFormula1724926316235 + implements MigrationInterface +{ + name = 'AddValueVariableFormula1724926316235'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "price_formulas" ADD "value_for" character varying NOT NULL DEFAULT 'dpp'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "price_formulas" DROP COLUMN "value_for"`, + ); + } +} diff --git a/src/database/migrations/1725962197762-add-payment-date-bank-column-at-transaction.ts b/src/database/migrations/1725962197762-add-payment-date-bank-column-at-transaction.ts new file mode 100644 index 0000000..f8f7789 --- /dev/null +++ b/src/database/migrations/1725962197762-add-payment-date-bank-column-at-transaction.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPaymentDateBankColumnAtTransaction1725962197762 + implements MigrationInterface +{ + name = 'AddPaymentDateBankColumnAtTransaction1725962197762'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" ADD "payment_date_bank" date`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "payment_date_bank"`, + ); + } +} diff --git a/src/database/migrations/1726033041774-add-pos-name-column.ts b/src/database/migrations/1726033041774-add-pos-name-column.ts new file mode 100644 index 0000000..a557191 --- /dev/null +++ b/src/database/migrations/1726033041774-add-pos-name-column.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPosNameColumn1726033041774 implements MigrationInterface { + name = 'AddPosNameColumn1726033041774'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" ADD "creator_counter_name" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "logs_pos" ADD "pos_name" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "logs_pos" DROP COLUMN "pos_name"`); + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "creator_counter_name"`, + ); + } +} diff --git a/src/database/migrations/1726041175749-add-flag-role-queue.ts b/src/database/migrations/1726041175749-add-flag-role-queue.ts new file mode 100644 index 0000000..7a22ad0 --- /dev/null +++ b/src/database/migrations/1726041175749-add-flag-role-queue.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddFlagRoleQueue1726041175749 implements MigrationInterface { + name = 'AddFlagRoleQueue1726041175749'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."users_role_enum" RENAME TO "users_role_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "public"."users_role_enum" AS ENUM('superadmin', 'staff', 'tenant', 'queue_admin')`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" TYPE "public"."users_role_enum" USING "role"::"text"::"public"."users_role_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'staff'`, + ); + await queryRunner.query(`DROP TYPE "public"."users_role_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."users_role_enum_old" AS ENUM('superadmin', 'staff', 'tenant')`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" TYPE "public"."users_role_enum_old" USING "role"::"text"::"public"."users_role_enum_old"`, + ); + await queryRunner.query( + `ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'staff'`, + ); + await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); + await queryRunner.query( + `ALTER TYPE "public"."users_role_enum_old" RENAME TO "users_role_enum"`, + ); + } +} diff --git a/src/database/migrations/1726045820711-add-tax-item-transaction.ts b/src/database/migrations/1726045820711-add-tax-item-transaction.ts new file mode 100644 index 0000000..7dffa6a --- /dev/null +++ b/src/database/migrations/1726045820711-add-tax-item-transaction.ts @@ -0,0 +1,73 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTaxItemTransaction1726045820711 implements MigrationInterface { + name = 'AddTaxItemTransaction1726045820711'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "transaction_item_taxes" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "tax_id" character varying, "tax_name" character varying, "taxt_value" numeric, "tax_total_value" numeric, "transaction_id" uuid, CONSTRAINT "PK_fc5f6da61b24eb5bfdd503b0a0d" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "t_breakdown_item_taxes" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "tax_id" character varying, "tax_name" character varying, "taxt_value" numeric, "tax_total_value" numeric, "transaction_id" uuid, CONSTRAINT "PK_a1ef08d2c68169a50102aa70eca" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "total_profit_share" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "total_profit_share" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_taxes" ADD CONSTRAINT "FK_f5c4966a381d903899cafb4b5ba" FOREIGN KEY ("transaction_id") REFERENCES "transaction_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "t_breakdown_item_taxes" ADD CONSTRAINT "FK_74bedce7e94f6707ddf26ef0c0f" FOREIGN KEY ("transaction_id") REFERENCES "transaction_item_breakdowns"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "payment_total_dpp" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "payment_total_dpp" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "payment_total_tax" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "payment_total_tax" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "total_share_tenant" numeric`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "t_breakdown_item_taxes" DROP CONSTRAINT "FK_74bedce7e94f6707ddf26ef0c0f"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_taxes" DROP CONSTRAINT "FK_f5c4966a381d903899cafb4b5ba"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "total_profit_share"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "total_profit_share"`, + ); + await queryRunner.query(`DROP TABLE "t_breakdown_item_taxes"`); + await queryRunner.query(`DROP TABLE "transaction_item_taxes"`); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "payment_total_dpp"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "payment_total_dpp"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "payment_total_tax"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "payment_total_tax"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "total_share_tenant"`, + ); + } +} diff --git a/src/database/migrations/1726115025759-add-table-user-login.ts b/src/database/migrations/1726115025759-add-table-user-login.ts new file mode 100644 index 0000000..5099208 --- /dev/null +++ b/src/database/migrations/1726115025759-add-table-user-login.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTableUserLogin1726115025759 implements MigrationInterface { + name = 'AddTableUserLogin1726115025759'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "users_login" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "login_date" bigint NOT NULL, "login_token" character varying, "user_id" uuid, CONSTRAINT "REL_2a80a213b51423ce5b8211f058" UNIQUE ("user_id"), CONSTRAINT "PK_e564194a9a22f8c623354284f75" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "FK_2a80a213b51423ce5b8211f0584" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "FK_2a80a213b51423ce5b8211f0584"`, + ); + await queryRunner.query(`DROP TABLE "users_login"`); + } +} diff --git a/src/database/migrations/1726122619596-update-table-user-login.ts b/src/database/migrations/1726122619596-update-table-user-login.ts new file mode 100644 index 0000000..4855807 --- /dev/null +++ b/src/database/migrations/1726122619596-update-table-user-login.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateTableUserLogin1726122619596 implements MigrationInterface { + name = 'UpdateTableUserLogin1726122619596'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "FK_2a80a213b51423ce5b8211f0584"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "FK_2a80a213b51423ce5b8211f0584" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "FK_2a80a213b51423ce5b8211f0584"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "FK_2a80a213b51423ce5b8211f0584" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/src/database/migrations/1726123955427-add-table-log-user-login.ts b/src/database/migrations/1726123955427-add-table-log-user-login.ts new file mode 100644 index 0000000..91a5788 --- /dev/null +++ b/src/database/migrations/1726123955427-add-table-log-user-login.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTableLogUserLogin1726123955427 implements MigrationInterface { + name = 'AddTableLogUserLogin1726123955427'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."log_users_login_type_enum" AS ENUM('login', 'logout')`, + ); + await queryRunner.query( + `CREATE TYPE "public"."log_users_login_role_enum" AS ENUM('superadmin', 'staff', 'tenant', 'queue_admin')`, + ); + await queryRunner.query( + `CREATE TABLE "log_users_login" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" "public"."log_users_login_type_enum", "role" "public"."log_users_login_role_enum", "user_id" uuid, "username" character varying, "created_at" bigint, CONSTRAINT "PK_75141588aa6ee560504f7d3adce" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "log_users_login"`); + await queryRunner.query(`DROP TYPE "public"."log_users_login_role_enum"`); + await queryRunner.query(`DROP TYPE "public"."log_users_login_type_enum"`); + } +} diff --git a/src/database/migrations/1726139426994-add-column-item_id.ts b/src/database/migrations/1726139426994-add-column-item_id.ts new file mode 100644 index 0000000..30ce865 --- /dev/null +++ b/src/database/migrations/1726139426994-add-column-item_id.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnItemId1726139426994 implements MigrationInterface { + name = 'AddColumnItemId1726139426994'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users_login" ADD "item_id" uuid`); + await queryRunner.query(`ALTER TABLE "log_users_login" ADD "item_id" uuid`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "log_users_login" DROP COLUMN "item_id"`, + ); + await queryRunner.query(`ALTER TABLE "users_login" DROP COLUMN "item_id"`); + } +} diff --git a/src/database/migrations/1726141393404-add-column-item_name.ts b/src/database/migrations/1726141393404-add-column-item_name.ts new file mode 100644 index 0000000..f67870b --- /dev/null +++ b/src/database/migrations/1726141393404-add-column-item_name.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnItemName1726141393404 implements MigrationInterface { + name = 'AddColumnItemName1726141393404'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users_login" ADD "item_name" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "log_users_login" ADD "item_name" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "log_users_login" DROP COLUMN "item_name"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" DROP COLUMN "item_name"`, + ); + } +} diff --git a/src/database/migrations/1726365023179-add-formula-to-tax.ts b/src/database/migrations/1726365023179-add-formula-to-tax.ts new file mode 100644 index 0000000..4654d9b --- /dev/null +++ b/src/database/migrations/1726365023179-add-formula-to-tax.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddFormulaToTax1726365023179 implements MigrationInterface { + name = 'AddFormulaToTax1726365023179'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "taxes" ADD "formula_render" json`); + await queryRunner.query( + `ALTER TABLE "taxes" ADD "formula_string" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "taxes" DROP COLUMN "formula_string"`); + await queryRunner.query(`ALTER TABLE "taxes" DROP COLUMN "formula_render"`); + } +} diff --git a/src/database/migrations/1726642119207-change-user-login-relation.ts b/src/database/migrations/1726642119207-change-user-login-relation.ts new file mode 100644 index 0000000..fe55b5a --- /dev/null +++ b/src/database/migrations/1726642119207-change-user-login-relation.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ChangeUserLoginRelation1726642119207 + implements MigrationInterface +{ + name = 'ChangeUserLoginRelation1726642119207'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "refresh_token"`); + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "FK_2a80a213b51423ce5b8211f0584"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "REL_2a80a213b51423ce5b8211f058"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "FK_2a80a213b51423ce5b8211f0584" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users_login" DROP CONSTRAINT "FK_2a80a213b51423ce5b8211f0584"`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "REL_2a80a213b51423ce5b8211f058" UNIQUE ("user_id")`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD CONSTRAINT "FK_2a80a213b51423ce5b8211f0584" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "users" ADD "refresh_token" character varying`, + ); + } +} diff --git a/src/database/migrations/1726642499135-add-column-source-at-user-login.ts b/src/database/migrations/1726642499135-add-column-source-at-user-login.ts new file mode 100644 index 0000000..7d48211 --- /dev/null +++ b/src/database/migrations/1726642499135-add-column-source-at-user-login.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnSourceAtUserLogin1726642499135 + implements MigrationInterface +{ + name = 'AddColumnSourceAtUserLogin1726642499135'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."users_login_role_enum" AS ENUM('superadmin', 'staff', 'tenant', 'queue_admin')`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD "role" "public"."users_login_role_enum"`, + ); + await queryRunner.query( + `CREATE TYPE "public"."users_login_source_enum" AS ENUM('POS_ADMIN', 'POS_COUNTER', 'QUEUE_ADMIN', 'QUEUE_CUSTOMER')`, + ); + await queryRunner.query( + `ALTER TABLE "users_login" ADD "source" "public"."users_login_source_enum"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users_login" DROP COLUMN "source"`); + await queryRunner.query(`DROP TYPE "public"."users_login_source_enum"`); + await queryRunner.query(`ALTER TABLE "users_login" DROP COLUMN "role"`); + await queryRunner.query(`DROP TYPE "public"."users_login_role_enum"`); + } +} diff --git a/src/database/migrations/1726647442006-add-source-on-log-login.ts b/src/database/migrations/1726647442006-add-source-on-log-login.ts new file mode 100644 index 0000000..2f5cf26 --- /dev/null +++ b/src/database/migrations/1726647442006-add-source-on-log-login.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSourceOnLogLogin1726647442006 implements MigrationInterface { + name = 'AddSourceOnLogLogin1726647442006'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."log_users_login_source_enum" AS ENUM('POS_ADMIN', 'POS_COUNTER', 'QUEUE_ADMIN', 'QUEUE_CUSTOMER')`, + ); + await queryRunner.query( + `ALTER TABLE "log_users_login" ADD "source" "public"."log_users_login_source_enum"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "log_users_login" DROP COLUMN "source"`, + ); + await queryRunner.query(`DROP TYPE "public"."log_users_login_source_enum"`); + } +} diff --git a/src/database/migrations/1726824289989-add-discount-for-item-transaction.ts b/src/database/migrations/1726824289989-add-discount-for-item-transaction.ts new file mode 100644 index 0000000..4e005a5 --- /dev/null +++ b/src/database/migrations/1726824289989-add-discount-for-item-transaction.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDiscountForItemTransaction1726824289989 + implements MigrationInterface +{ + name = 'AddDiscountForItemTransaction1726824289989'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "subtotal" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "discount_value" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "subtotal" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "discount_value" numeric`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" ADD "total_price" numeric`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "total_price"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "discount_value"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "subtotal"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "discount_value"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "subtotal"`, + ); + } +} diff --git a/src/database/migrations/1726830293878-change-column-name.ts b/src/database/migrations/1726830293878-change-column-name.ts new file mode 100644 index 0000000..0489ee9 --- /dev/null +++ b/src/database/migrations/1726830293878-change-column-name.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ChangeColumnName1726830293878 implements MigrationInterface { + name = 'ChangeColumnName1726830293878'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transaction_items" RENAME COLUMN "subtotal" TO "total_net_price"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" RENAME COLUMN "subtotal" TO "total_net_price"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transaction_item_breakdowns" RENAME COLUMN "total_net_price" TO "subtotal"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" RENAME COLUMN "total_net_price" TO "subtotal"`, + ); + } +} diff --git a/src/modules/configuration/auth/auth.module.ts b/src/modules/configuration/auth/auth.module.ts index 84b5ed8..82e0c62 100644 --- a/src/modules/configuration/auth/auth.module.ts +++ b/src/modules/configuration/auth/auth.module.ts @@ -9,14 +9,32 @@ import { CqrsModule } from '@nestjs/cqrs'; import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { UserDataService } from 'src/modules/user-related/user/data/services/user-data.service'; +import { AuthAdminQueueController } from './infrastructure/auth-admin-queue.controller'; +import { AuthAdminQueueOrchestrator } from './domain/auth-admin-queue.orchestrator'; +import { LoginAdminQueueManager } from './domain/managers/admin-queue/login-admin-queue.manager'; +import { LogoutAdminQueueManager } from './domain/managers/admin-queue/logout-admin-queue.manager'; +import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-login.model'; @Module({ imports: [ ConfigModule.forRoot(), - TypeOrmModule.forFeature([UserModel], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [UserModel, UserLoginModel], + CONNECTION_NAME.DEFAULT, + ), CqrsModule, ], - controllers: [AuthController], - providers: [LoginManager, LogoutManager, UserDataService, AuthOrchestrator], + controllers: [AuthController, AuthAdminQueueController], + providers: [ + LoginManager, + LogoutManager, + UserDataService, + AuthOrchestrator, + + // ADMIN QUEUE + AuthAdminQueueOrchestrator, + LoginAdminQueueManager, + LogoutAdminQueueManager, + ], }) export class AuthModule {} diff --git a/src/modules/configuration/auth/domain/auth-admin-queue.orchestrator.ts b/src/modules/configuration/auth/domain/auth-admin-queue.orchestrator.ts new file mode 100644 index 0000000..dc17f9a --- /dev/null +++ b/src/modules/configuration/auth/domain/auth-admin-queue.orchestrator.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { LoginAdminQueueManager } from './managers/admin-queue/login-admin-queue.manager'; +import { LogoutAdminQueueManager } from './managers/admin-queue/logout-admin-queue.manager'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { UserDataService } from 'src/modules/user-related/user/data/services/user-data.service'; + +@Injectable() +export class AuthAdminQueueOrchestrator { + constructor( + private loginManager: LoginAdminQueueManager, + private logoutManager: LogoutAdminQueueManager, + private serviceData: UserDataService, + ) {} + + async login(data): Promise { + this.loginManager.setData(data); + this.loginManager.setService(this.serviceData, TABLE_NAME.USER); + await this.loginManager.execute(); + return this.loginManager.getResult(); + } + + async logout(userId?: string): Promise { + if (userId) this.logoutManager.setData({ user_id: userId }); + this.logoutManager.setService(this.serviceData, TABLE_NAME.USER); + await this.logoutManager.execute(); + return this.logoutManager.getResult(); + } + + async forceLogout(token): Promise { + return this.serviceData.forceLogout(token); + } +} diff --git a/src/modules/configuration/auth/domain/auth.orchestrator.ts b/src/modules/configuration/auth/domain/auth.orchestrator.ts index 1be43e4..f1d3efd 100644 --- a/src/modules/configuration/auth/domain/auth.orchestrator.ts +++ b/src/modules/configuration/auth/domain/auth.orchestrator.ts @@ -24,4 +24,8 @@ export class AuthOrchestrator { await this.logoutManager.execute(); return this.logoutManager.getResult(); } + + async forceLogout(token): Promise { + return this.serviceData.forceLogout(token); + } } diff --git a/src/modules/configuration/auth/domain/managers/admin-queue/login-admin-queue.manager.ts b/src/modules/configuration/auth/domain/managers/admin-queue/login-admin-queue.manager.ts new file mode 100644 index 0000000..cc6ecab --- /dev/null +++ b/src/modules/configuration/auth/domain/managers/admin-queue/login-admin-queue.manager.ts @@ -0,0 +1,167 @@ +import { + HttpStatus, + Inject, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { validatePassword } from 'src/core/helpers/password/bcrypt.helpers'; +import { BaseCustomManager } from 'src/core/modules/domain/usecase/managers/base-custom.manager'; +import { SessionService } from 'src/core/sessions'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { EventTopics } from 'src/core/strings/constants/interface.constants'; +import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; +import { UserEntity } from 'src/modules/user-related/user/domain/entities/user.entity'; +import { In } from 'typeorm'; +import { UserRole } from 'src/modules/user-related/user/constants'; +import { AppSource, LogUserType } from 'src/core/helpers/constant'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; +import { UserLoginEntity } from 'src/modules/user-related/user/domain/entities/user-login.entity'; + +@Injectable() +export class LoginAdminQueueManager extends BaseCustomManager { + @Inject() + protected session: SessionService; + protected token; + protected userLogin; + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async process(): Promise { + const itemLogin = await this.dataService.getLoginUserByItem( + this.data.item_id, + ); + + // get user active by username + this.userLogin = await this.dataService.getOneByOptions({ + where: { + username: this.data.username, + status: STATUS.ACTIVE, + role: In([UserRole.QUEUE_ADMIN, UserRole.SUPERADMIN]), + }, + relations: ['user_login'], + }); + + if (!this.userLogin) this.throwError(); + + // validasi password + const valid = await validatePassword( + this.data.password, + this.userLogin?.password, + ); + if (!valid) this.throwError(); + + const userLoginItem = await this.dataService.getOneByOptions({ + where: { + id: itemLogin?.user_id, + }, + }); + + const hasLoginAsQueue = this.userLogin?.user_login?.find( + (item) => item.source === AppSource.QUEUE_ADMIN, + ); + + if (hasLoginAsQueue && hasLoginAsQueue?.item_id !== this.data.item_id) { + throw new UnauthorizedException({ + statusCode: HttpStatus.UNAUTHORIZED, + message: `Akun anda sudah login di item "${hasLoginAsQueue?.item_name}"`, + error: 'Unauthorized', + }); + } else if (itemLogin && itemLogin.user_id !== this.userLogin.id) { + throw new UnauthorizedException({ + statusCode: HttpStatus.UNAUTHORIZED, + message: `"${userLoginItem.name}" masih login sebagai admin antrian `, + error: 'Unauthorized', + }); + } + + // * Disini untuk isi token + const tokenData = { + id: this.userLogin.id, + name: this.userLogin.name, + username: this.userLogin.username, + role: this.userLogin.role, + user_privilege_id: this.userLogin.user_privilege_id, + item_id: this.data.item_id, + item_name: this.data.item_name, + source: AppSource.QUEUE_ADMIN, + }; + + Logger.debug('Sign Token Admin Queue', 'LoginAdminQueueManager'); + this.token = this.session.createAccessToken(tokenData); + + Logger.debug('Save Login Token', 'LoginManager'); + const userLoginData: UserLoginEntity = { + user_id: this.userLogin.id, + login_token: this.token, + login_date: new Date().getTime(), + source: AppSource.QUEUE_ADMIN, + role: this.userLogin.role, + item_id: this.data.item_id, + item_name: this.data.item_name, + }; + if (hasLoginAsQueue?.item_id === this.data.item_id) { + Object.assign(userLoginData, { id: hasLoginAsQueue.id }); + } + // Update refresh token + await this.dataService.saveUserLogin(userLoginData); + + await this.publishEvents(); + + Logger.debug('Process Login Admin Queue Done', 'LoginAdminQueueManager'); + return; + } + + async afterProcess(): Promise { + return; + } + + getResult() { + return { + id: this.userLogin.id, + name: this.userLogin.name, + username: this.userLogin.username, + role: this.userLogin.role, + token: this.token, + item_id: this.data.item_id, + item_name: this.data.item_name, + }; + } + + get entityTarget(): any { + return UserModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: LogUserLoginEvent, + data: { + type: LogUserType.login, + role: this.userLogin.role, + user_id: this.userLogin.id, + username: this.userLogin.username, + created_at: new Date().getTime(), + item_id: this.data.item_id, + item_name: this.data.item_name, + source: AppSource.QUEUE_ADMIN, + }, + }, + ]; + } + + // !throw errornya akan sama, untuk security + throwError() { + throw new UnauthorizedException({ + statusCode: HttpStatus.UNAUTHORIZED, + message: `Gagal! username atau password tidak sesuai`, + error: 'Unauthorized', + }); + } +} diff --git a/src/modules/configuration/auth/domain/managers/admin-queue/logout-admin-queue.manager.ts b/src/modules/configuration/auth/domain/managers/admin-queue/logout-admin-queue.manager.ts new file mode 100644 index 0000000..4475ad0 --- /dev/null +++ b/src/modules/configuration/auth/domain/managers/admin-queue/logout-admin-queue.manager.ts @@ -0,0 +1,62 @@ +import { AppSource, LogUserType } from 'src/core/helpers/constant'; +import { BaseCustomManager } from 'src/core/modules/domain/usecase/managers/base-custom.manager'; +import { EventTopics } from 'src/core/strings/constants/interface.constants'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; +import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; +import { UserEntity } from 'src/modules/user-related/user/domain/entities/user.entity'; + +export class LogoutAdminQueueManager extends BaseCustomManager { + protected userLogin; + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async process(): Promise { + const id = this.data?.user_id ?? this.user.id; + + this.userLogin = await this.dataService.getOneByOptions({ + where: { id }, + }); + + await this.dataService.removeUserLogin({ + user_id: id, + source: AppSource.QUEUE_ADMIN, + }); + + await this.publishEvents(); + return; + } + + async afterProcess(): Promise { + return; + } + + getResult() { + return `Success Logout User`; + } + + get entityTarget(): any { + return UserModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: LogUserLoginEvent, + data: { + type: LogUserType.logout, + role: this.userLogin.role, + user_id: this.userLogin.id, + username: this.userLogin.name, + created_at: new Date().getTime(), + source: AppSource.QUEUE_ADMIN, + }, + }, + ]; + } +} diff --git a/src/modules/configuration/auth/domain/managers/login.manager.ts b/src/modules/configuration/auth/domain/managers/login.manager.ts index d6ab1b3..a773b9e 100644 --- a/src/modules/configuration/auth/domain/managers/login.manager.ts +++ b/src/modules/configuration/auth/domain/managers/login.manager.ts @@ -12,7 +12,11 @@ import { STATUS } from 'src/core/strings/constants/base.constants'; import { EventTopics } from 'src/core/strings/constants/interface.constants'; import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; import { UserEntity } from 'src/modules/user-related/user/domain/entities/user.entity'; -import { UserLoginEvent } from '../entities/login.event'; +import { Not } from 'typeorm'; +import { UserRole } from 'src/modules/user-related/user/constants'; +import { AppSource, LogUserType } from 'src/core/helpers/constant'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; +import { UserLoginEntity } from 'src/modules/user-related/user/domain/entities/user-login.entity'; @Injectable() export class LoginManager extends BaseCustomManager { @@ -36,6 +40,7 @@ export class LoginManager extends BaseCustomManager { where: { username: this.data.username, status: STATUS.ACTIVE, + role: Not(UserRole.QUEUE_ADMIN), }, relations: [ 'user_privilege', @@ -58,25 +63,27 @@ export class LoginManager extends BaseCustomManager { username: this.userLogin.username, role: this.userLogin.role, user_privilege_id: this.userLogin.user_privilege_id, + source: AppSource.POS_ADMIN, }; Logger.debug('Sign Token', 'LoginManager'); this.token = this.session.createAccessToken(tokenData); - Logger.debug('refreshToken', 'LoginManager'); - const refreshToken = this.session.createAccessToken(tokenData); + Logger.debug('Save Login Token', 'LoginManager'); + const userLoginData: UserLoginEntity = { + user_id: this.userLogin.id, + login_token: this.token, + login_date: new Date().getTime(), + source: AppSource.POS_ADMIN, + role: this.userLogin.role, + item_id: null, + item_name: null, + }; - Logger.debug('Update Refresh Token', 'LoginManager'); // Update refresh token - await this.dataService.update( - this.queryRunner, - this.entityTarget, - { id: this.userLogin.id }, - { - refresh_token: refreshToken, - }, - ); + await this.dataService.saveUserLogin(userLoginData); + await this.publishEvents(); Logger.debug('Process Login Done', 'LoginManager'); return; } @@ -119,11 +126,14 @@ export class LoginManager extends BaseCustomManager { get eventTopics(): EventTopics[] { return [ { - topic: UserLoginEvent, + topic: LogUserLoginEvent, data: { - id: this.userLogin.id, - type: 'login', - timestamp: new Date().getTime(), + type: LogUserType.login, + role: this.userLogin.role, + user_id: this.userLogin.id, + username: this.userLogin.username, + created_at: new Date().getTime(), + source: AppSource.POS_ADMIN, }, }, ]; diff --git a/src/modules/configuration/auth/domain/managers/logout.manager.ts b/src/modules/configuration/auth/domain/managers/logout.manager.ts index 482a84c..9586750 100644 --- a/src/modules/configuration/auth/domain/managers/logout.manager.ts +++ b/src/modules/configuration/auth/domain/managers/logout.manager.ts @@ -2,7 +2,8 @@ import { BaseCustomManager } from 'src/core/modules/domain/usecase/managers/base import { EventTopics } from 'src/core/strings/constants/interface.constants'; import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; import { UserEntity } from 'src/modules/user-related/user/domain/entities/user.entity'; -import { UserLogoutEvent } from '../entities/logout.event'; +import { AppSource, LogUserType } from 'src/core/helpers/constant'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; export class LogoutManager extends BaseCustomManager { async validateProcess(): Promise { @@ -14,15 +15,12 @@ export class LogoutManager extends BaseCustomManager { } async process(): Promise { - await this.dataService.update( - this.queryRunner, - this.entityTarget, - { id: this.user.id }, - { - refresh_token: null, - }, - ); - + await this.dataService.removeUserLogin({ + user_id: this.user.id, + login_token: this.userProvider.token, + source: AppSource.POS_ADMIN, + }); + await this.publishEvents(); return; } @@ -41,11 +39,14 @@ export class LogoutManager extends BaseCustomManager { get eventTopics(): EventTopics[] { return [ { - topic: UserLogoutEvent, + topic: LogUserLoginEvent, data: { - id: this.user.id, - type: 'logout', - timestamp: new Date().getTime(), + type: LogUserType.logout, + role: this.user.role, + user_id: this.user.id, + username: this.user.name, + created_at: new Date().getTime(), + source: AppSource.POS_ADMIN, }, }, ]; diff --git a/src/modules/configuration/auth/infrastructure/auth-admin-queue.controller.ts b/src/modules/configuration/auth/infrastructure/auth-admin-queue.controller.ts new file mode 100644 index 0000000..9da9a14 --- /dev/null +++ b/src/modules/configuration/auth/infrastructure/auth-admin-queue.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Delete, Param, Post, Put } from '@nestjs/common'; +import { ExcludePrivilege, Public } from 'src/core/guards'; +import { ApiBearerAuth } from '@nestjs/swagger'; +import { ForceLogoutDto, LoginQueueDto } from './dto/login.dto'; +import { AuthAdminQueueOrchestrator } from '../domain/auth-admin-queue.orchestrator'; + +@Controller('v1/auth/queue') +export class AuthAdminQueueController { + constructor(private orchestrator: AuthAdminQueueOrchestrator) {} + + @Post() + @Public(true) + async login(@Body() body: LoginQueueDto) { + return await this.orchestrator.login(body); + } + + @ApiBearerAuth('JWT') + @Public(false) + @ExcludePrivilege() + @Delete('logout') + async logout() { + return await this.orchestrator.logout(); + } + + @Put(':user_id/logout') + async logoutQueueAdmin(@Param('user_id') userId: string) { + return await this.orchestrator.logout(userId); + } + + @Post('force-logout') + @Public(true) + async forceLogout(@Body() body: ForceLogoutDto) { + return await this.orchestrator.forceLogout(body.token); + } +} diff --git a/src/modules/configuration/auth/infrastructure/auth.controller.ts b/src/modules/configuration/auth/infrastructure/auth.controller.ts index ef3bd7c..0c0f0ae 100644 --- a/src/modules/configuration/auth/infrastructure/auth.controller.ts +++ b/src/modules/configuration/auth/infrastructure/auth.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Delete, Post } from '@nestjs/common'; -import { Public } from 'src/core/guards'; +import { ExcludePrivilege, Public } from 'src/core/guards'; import { AuthOrchestrator } from '../domain/auth.orchestrator'; import { ApiBearerAuth } from '@nestjs/swagger'; -import { LoginDto } from './dto/login.dto'; +import { ForceLogoutDto, LoginDto } from './dto/login.dto'; @Controller('v1/auth') export class AuthController { @@ -16,8 +16,15 @@ export class AuthController { @ApiBearerAuth('JWT') @Public(false) + @ExcludePrivilege() @Delete('logout') - async logoout() { + async logout() { return await this.orchestrator.logout(); } + + @Post('force-logout') + @Public(true) + async forceLogout(@Body() body: ForceLogoutDto) { + return await this.orchestrator.forceLogout(body.token); + } } diff --git a/src/modules/configuration/auth/infrastructure/dto/login.dto.ts b/src/modules/configuration/auth/infrastructure/dto/login.dto.ts index 267d433..145437c 100644 --- a/src/modules/configuration/auth/infrastructure/dto/login.dto.ts +++ b/src/modules/configuration/auth/infrastructure/dto/login.dto.ts @@ -11,3 +11,27 @@ export class LoginDto implements LoginRequest { @IsString() password: string; } + +export class LoginQueueDto implements LoginRequest { + @ApiProperty({ name: 'username', required: true, default: 'superadmin' }) + @IsString() + username: string; + + @ApiProperty({ name: 'password', required: true, default: 'Eigen123!' }) + @IsString() + password: string; + + @ApiProperty({ name: 'item_id', required: true, default: 'string' }) + @IsString() + item_id: string; + + @ApiProperty({ name: 'item_name', required: true, default: 'string' }) + @IsString() + item_name: string; +} + +export class ForceLogoutDto { + @ApiProperty({ required: true }) + @IsString() + token: string; +} diff --git a/src/modules/configuration/couch/couch.module.ts b/src/modules/configuration/couch/couch.module.ts index a258817..6fd848f 100644 --- a/src/modules/configuration/couch/couch.module.ts +++ b/src/modules/configuration/couch/couch.module.ts @@ -51,6 +51,7 @@ import { import { SeasonPeriodDataService } from 'src/modules/season-related/season-period/data/services/season-period-data.service'; import { SeasonPeriodModel } from 'src/modules/season-related/season-period/data/models/season-period.model'; import { TransactionDemographyModel } from 'src/modules/transaction/transaction/data/models/transaction-demography.model'; +import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-login.model'; @Module({ imports: [ @@ -61,6 +62,7 @@ import { TransactionDemographyModel } from 'src/modules/transaction/transaction/ ItemRateModel, SeasonPeriodModel, UserModel, + UserLoginModel, TransactionModel, TransactionTaxModel, TransactionItemModel, @@ -100,5 +102,6 @@ import { TransactionDemographyModel } from 'src/modules/transaction/transaction/ ItemDataService, CouchService, ], + exports: [CouchService], }) export class CouchModule {} diff --git a/src/modules/configuration/couch/data/services/couch.service.ts b/src/modules/configuration/couch/data/services/couch.service.ts index 3054cd8..60f031d 100644 --- a/src/modules/configuration/couch/data/services/couch.service.ts +++ b/src/modules/configuration/couch/data/services/couch.service.ts @@ -23,13 +23,14 @@ export class CouchService { for (const database of DatabaseListen) { const db = nano.db.use(database); db.changesReader.start({ includeDocs: true }).on('change', (change) => { - Logger.log( + Logger.verbose( `Receive Data from ${database}: ${change?.id}`, 'CouchService', ); this.changeDoc(change, database); }); + // transaction Logger.log(`start listen database ${database}`, 'CouchService'); } } diff --git a/src/modules/configuration/log/data/models/log-user-login.model.ts b/src/modules/configuration/log/data/models/log-user-login.model.ts new file mode 100644 index 0000000..e99a3b1 --- /dev/null +++ b/src/modules/configuration/log/data/models/log-user-login.model.ts @@ -0,0 +1,37 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { UserEntity } from '../../../../user-related/user/domain/entities/user.entity'; +import { Column, Entity } from 'typeorm'; +import { BaseCoreModel } from 'src/core/modules/data/model/base-core.model'; +import { LogUserLoginEntity } from '../../domain/entities/log-user-login.entity'; +import { UserRole } from '../../../../user-related/user/constants'; +import { AppSource, LogUserType } from 'src/core/helpers/constant'; + +@Entity(TABLE_NAME.LOG_USER_LOGIN) +export class LogUserLoginModel + extends BaseCoreModel + implements LogUserLoginEntity +{ + @Column({ type: 'enum', enum: LogUserType, nullable: true }) + type: LogUserType; + + @Column({ type: 'enum', enum: UserRole, nullable: true }) + role: UserRole; + + @Column({ type: 'uuid', nullable: true }) + user_id: string; + + @Column({ type: 'uuid', nullable: true }) + item_id: string; + + @Column({ type: 'varchar', nullable: true }) + item_name: string; + + @Column({ type: 'varchar', nullable: true }) + username: string; + + @Column({ type: 'bigint', nullable: true }) + created_at: number; + + @Column({ type: 'enum', enum: AppSource, nullable: true }) + source: AppSource; +} diff --git a/src/modules/configuration/log/data/models/pos-log.model.ts b/src/modules/configuration/log/data/models/pos-log.model.ts index fd8631f..2d09858 100644 --- a/src/modules/configuration/log/data/models/pos-log.model.ts +++ b/src/modules/configuration/log/data/models/pos-log.model.ts @@ -14,6 +14,9 @@ export class PosLogModel @Column('bigint', { name: 'pos_number', nullable: true }) pos_number: number; + @Column('varchar', { name: 'pos_name', nullable: true }) + pos_name: string; + @Column('decimal', { name: 'total_balance', nullable: true }) total_balance: number; diff --git a/src/modules/configuration/log/data/services/log-user-login.service.ts b/src/modules/configuration/log/data/services/log-user-login.service.ts new file mode 100644 index 0000000..5d1865c --- /dev/null +++ b/src/modules/configuration/log/data/services/log-user-login.service.ts @@ -0,0 +1,21 @@ +import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { LogUserLoginEntity } from '../../domain/entities/log-user-login.entity'; +import { LogUserLoginModel } from '../models/log-user-login.model'; + +@Injectable() +export class LogUserLoginService extends BaseDataService { + constructor( + @InjectRepository(LogUserLoginModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } + + async saveData(data) { + this.repo.save(data); + } +} diff --git a/src/modules/configuration/log/domain/entities/log-user-login.entity.ts b/src/modules/configuration/log/domain/entities/log-user-login.entity.ts new file mode 100644 index 0000000..c68ab17 --- /dev/null +++ b/src/modules/configuration/log/domain/entities/log-user-login.entity.ts @@ -0,0 +1,13 @@ +import { LogUserType } from 'src/core/helpers/constant'; +import { UserRole } from '../../../../user-related/user/constants'; +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; + +export interface LogUserLoginEntity extends BaseCoreEntity { + type: LogUserType; + role: UserRole; + user_id: string; + item_id: string; + item_name: string; + username: string; + created_at: number; +} diff --git a/src/modules/configuration/log/domain/entities/log-user-login.event.ts b/src/modules/configuration/log/domain/entities/log-user-login.event.ts new file mode 100644 index 0000000..813681f --- /dev/null +++ b/src/modules/configuration/log/domain/entities/log-user-login.event.ts @@ -0,0 +1,5 @@ +import { IEvent } from 'src/core/strings/constants/interface.constants'; + +export class LogUserLoginEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/configuration/log/domain/entities/pos-log.entity.ts b/src/modules/configuration/log/domain/entities/pos-log.entity.ts index f29f780..360e071 100644 --- a/src/modules/configuration/log/domain/entities/pos-log.entity.ts +++ b/src/modules/configuration/log/domain/entities/pos-log.entity.ts @@ -3,6 +3,7 @@ import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entit export interface PosLogEntity extends BaseCoreEntity { type: PosLogType; pos_number: number; + pos_name: string; total_balance: number; created_at: number; creator_name: string; diff --git a/src/modules/configuration/log/domain/handlers/log-user-login.handler.ts b/src/modules/configuration/log/domain/handlers/log-user-login.handler.ts new file mode 100644 index 0000000..c22ec5d --- /dev/null +++ b/src/modules/configuration/log/domain/handlers/log-user-login.handler.ts @@ -0,0 +1,14 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; + +import { LogUserLoginEvent } from '../entities/log-user-login.event'; +import { LogUserLoginService } from '../../data/services/log-user-login.service'; + +@EventsHandler(LogUserLoginEvent) +export class LogUserLoginHandler implements IEventHandler { + constructor(private service: LogUserLoginService) {} + + async handle(event: LogUserLoginEvent) { + const data = event.data.data; + await this.service.saveData(data); + } +} diff --git a/src/modules/configuration/log/domain/handlers/pos-log.handler.ts b/src/modules/configuration/log/domain/handlers/pos-log.handler.ts index 192247f..db7c944 100644 --- a/src/modules/configuration/log/domain/handlers/pos-log.handler.ts +++ b/src/modules/configuration/log/domain/handlers/pos-log.handler.ts @@ -26,6 +26,7 @@ export class RecordPosLogHandler implements IEventHandler { type: PosLogType[data.type], total_balance: data.withdrawal_cash ?? data.opening_cash_balance, pos_number: data.pos_number, + pos_name: data.pos_name, creator_id: data.pos_admin?.id, creator_name: data.pos_admin?.name ?? data.pos_admin?.username, drawn_by_id: data.withdraw_user?.id, diff --git a/src/modules/configuration/log/log.module.ts b/src/modules/configuration/log/log.module.ts index 06002f9..7eaa8cc 100644 --- a/src/modules/configuration/log/log.module.ts +++ b/src/modules/configuration/log/log.module.ts @@ -12,12 +12,15 @@ import { LogService } from './data/services/log.service'; import { PosLogModel } from './data/models/pos-log.model'; import { PosLogService } from './data/services/pos-log.service'; import { RecordPosLogHandler } from './domain/handlers/pos-log.handler'; +import { LogUserLoginModel } from './data/models/log-user-login.model'; +import { LogUserLoginService } from './data/services/log-user-login.service'; +import { LogUserLoginHandler } from './domain/handlers/log-user-login.handler'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature( - [LogModel, ErrorLogModel, PosLogModel], + [LogModel, ErrorLogModel, PosLogModel, LogUserLoginModel], CONNECTION_NAME.DEFAULT, ), CqrsModule, @@ -27,10 +30,12 @@ import { RecordPosLogHandler } from './domain/handlers/pos-log.handler'; RecordLogHandler, RecordPosLogHandler, RecordErrorLogHandler, + LogUserLoginHandler, LogService, PosLogService, ErrorLogService, + LogUserLoginService, ], }) export class LogModule {} diff --git a/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts b/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts index 355c75b..f71341f 100644 --- a/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts +++ b/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts @@ -32,7 +32,6 @@ export class PaymentTransactionHandler const current_data = event.data.data; const data_id = current_data.transaction_id ?? event.data.id; const from_refund = event.data.module == TABLE_NAME.REFUND; - console.log('payment handlet', { data_id }); const payments = await this.paymentService.getManyByOptions({ where: { @@ -106,6 +105,15 @@ export class PaymentTransactionHandler `; })} `, + + refund_items_data: transaction?.['refund']?.refund_items + ?.filter((item) => Number(item.qty_refund) > 0) + .map((item) => { + return { + qty_refund: item.qty_refund, + item_name: item.transaction_item.item_name, + }; + }), }); } diff --git a/src/modules/configuration/mail/domain/helpers/send-email.helper.ts b/src/modules/configuration/mail/domain/helpers/send-email.helper.ts index ad0532a..2db8cd0 100644 --- a/src/modules/configuration/mail/domain/helpers/send-email.helper.ts +++ b/src/modules/configuration/mail/domain/helpers/send-email.helper.ts @@ -17,7 +17,7 @@ export async function sendEmail(receivers, invoiceType, attachment?) { for (const receiver of receivers) { try { const templateName = getTemplate(receiver.payment_type, invoiceType); - const templatePath = `./assets/email-template/${templateName}.html`; + const templatePath = `./assets/email-template/redesign/${templateName}.html`; const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); diff --git a/src/modules/gates/domain/entity/gate-request.entity.ts b/src/modules/gates/domain/entity/gate-request.entity.ts new file mode 100644 index 0000000..299e086 --- /dev/null +++ b/src/modules/gates/domain/entity/gate-request.entity.ts @@ -0,0 +1,5 @@ +export interface GateScanEntity { + gate_id: string; + type: string; + uuid: string; +} diff --git a/src/modules/gates/domain/entity/gate-response.entity.ts b/src/modules/gates/domain/entity/gate-response.entity.ts new file mode 100644 index 0000000..1d8019d --- /dev/null +++ b/src/modules/gates/domain/entity/gate-response.entity.ts @@ -0,0 +1,8 @@ +export interface GateResponseEntity { + code: number; + message: string; +} + +export interface GateMasterEntity { + codes: string[]; +} diff --git a/src/modules/gates/gate.module.ts b/src/modules/gates/gate.module.ts new file mode 100644 index 0000000..0f2577c --- /dev/null +++ b/src/modules/gates/gate.module.ts @@ -0,0 +1,17 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { GateController } from './infrastructure/gate.controller'; + +@Global() +@Module({ + imports: [ + ConfigModule.forRoot(), + // TypeOrmModule.forFeature( + // [], + // CONNECTION_NAME.DEFAULT, + // ), + ], + controllers: [GateController], + providers: [], +}) +export class GateScanModule {} diff --git a/src/modules/gates/infrastructure/gate.controller.ts b/src/modules/gates/infrastructure/gate.controller.ts new file mode 100644 index 0000000..74fa973 --- /dev/null +++ b/src/modules/gates/infrastructure/gate.controller.ts @@ -0,0 +1,94 @@ +import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { ApiTags } from '@nestjs/swagger'; +import { Public } from 'src/core/guards'; +import { GateScanEntity } from '../domain/entity/gate-request.entity'; +import { + GateMasterEntity, + GateResponseEntity, +} from '../domain/entity/gate-response.entity'; +import { Gate } from 'src/core/response'; + +const masterGates = [ + '319b6d3e-b661-4d19-8695-0dd6fb76465e', + '9afdb79d-7162-43e6-8ac6-f1941adea7ba', + '7e4c0281-8cf2-420e-aba1-c8ff834de450', + '19318ac8-caa0-47e4-8a41-2aac238d3665', + '495bc25f-42c4-4007-8e79-3747fa1054b6', + 'b90fc9a9-efd9-4216-a8af-7ed120b141de', + '4399e93c-a839-4802-a49d-f933c72b1433', + '970673a7-6370-444a-931a-9784220dd35d', + '151ab50e-4e54-4252-b3ab-f5c0817b27a0', + '4c0e6924-baf5-47fb-a15b-fd1cd0958cc0', +]; + +const failedGates = [ + 'b3c3ae7b-daf5-4340-998b-ee35ed41323d', + 'be157609-92b8-4989-920d-a81769bcb05a', +]; + +const gateResponses = [ + { + statusCode: 200, + code: 1, + message: 'Berhasil Check In', + }, + { + statusCode: 403, + code: 2, + message: 'Gagal melakukan Check In. Karena tiket telah kadaluarsa', + }, + { + statusCode: 403, + code: 3, + message: 'Gagal melakukan Check In. Tiket tidak tersedia', + }, +]; + +@ApiTags(`Gate - read`) +@Controller(`v1/gate`) +@Public(true) +@Gate() +export class GateController { + @Post('scan') + async scan( + @Body() data: GateScanEntity, + @Res({ passthrough: true }) res: Response, + ): Promise { + console.log(data); + if (masterGates.includes(data.uuid)) { + res.status(200); + return gateResponses[0]; + } + if (failedGates.includes(data.uuid)) { + res.status(403); + return gateResponses[2]; + } + + const response = Math.floor(Math.random() * 3); + const responseValue = gateResponses[response]; + + res.status(responseValue.statusCode); + return responseValue; + } + + @Get(':id/master') + async detail(@Param('id') id: string): Promise { + if (id == '1') return { codes: masterGates }; + return { + codes: this.createRandomStringArray(masterGates), + }; + } + + createRandomStringArray(inputArray: string[]): string[] { + const randomLength = Math.floor(Math.random() * 4) + 2; // Random length between 2 and 5 + const outputArray: string[] = []; + + while (outputArray.length < randomLength) { + const randomIndex = Math.floor(Math.random() * inputArray.length); + outputArray.push(inputArray[randomIndex]); + } + + return outputArray; + } +} diff --git a/src/modules/item-related/item/domain/usecases/item-read.orchestrator.ts b/src/modules/item-related/item/domain/usecases/item-read.orchestrator.ts index 5f7a8eb..b5aa996 100644 --- a/src/modules/item-related/item/domain/usecases/item-read.orchestrator.ts +++ b/src/modules/item-related/item/domain/usecases/item-read.orchestrator.ts @@ -10,11 +10,13 @@ import { ItemRateReadService } from 'src/modules/item-related/item-rate/data/ser import { FilterItemRateDto } from 'src/modules/item-related/item-rate/infrastructure/dto/filter-item-rate.dto'; import { ItemRateEntity } from 'src/modules/item-related/item-rate/domain/entities/item-rate.entity'; import { IndexItemRatesManager } from './managers/index-item-rates.manager'; +import { IndexItemQueueManager } from './managers/index-queue-item.manager'; @Injectable() export class ItemReadOrchestrator extends BaseReadOrchestrator { constructor( private indexManager: IndexItemManager, + private indexQueueManager: IndexItemQueueManager, private detailManager: DetailItemManager, private indexRateManager: IndexItemRatesManager, private serviceData: ItemReadService, @@ -30,6 +32,13 @@ export class ItemReadOrchestrator extends BaseReadOrchestrator { return this.indexManager.getResult(); } + async indexQueue(params): Promise> { + this.indexQueueManager.setFilterParam(params); + this.indexQueueManager.setService(this.serviceData, TABLE_NAME.ITEM); + await this.indexQueueManager.execute(); + return this.indexQueueManager.getResult(); + } + async detail(dataId: string): Promise { this.detailManager.setData(dataId); this.detailManager.setService(this.serviceData, TABLE_NAME.ITEM); diff --git a/src/modules/item-related/item/domain/usecases/managers/index-queue-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-queue-item.manager.ts new file mode 100644 index 0000000..b1fe5b3 --- /dev/null +++ b/src/modules/item-related/item/domain/usecases/managers/index-queue-item.manager.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager'; +import { ItemEntity } from '../../entities/item.entity'; +import { SelectQueryBuilder } from 'typeorm'; +import { + Param, + RelationParam, +} from 'src/core/modules/domain/entities/base-filter.entity'; +import { STATUS } from 'src/core/strings/constants/base.constants'; + +@Injectable() +export class IndexItemQueueManager extends BaseIndexManager { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + // relation only join (for query purpose) + joinRelations: [], + + // relation join and select (relasi yang ingin ditampilkan), + selectRelations: [], + + // relation yang hanya ingin dihitung (akan return number) + countRelations: [], + }; + } + + get selects(): string[] { + return [ + `${this.tableName}.id`, + `${this.tableName}.created_at`, + `${this.tableName}.name`, + ]; + } + + get specificFilter(): Param[] { + return [ + { + cols: `${this.tableName}.name`, + data: this.filterParam.names, + }, + ]; + } + + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + queryBuilder.andWhere(`${this.tableName}.status = :status`, { + status: STATUS.ACTIVE, + }); + + queryBuilder.andWhere(`${this.tableName}.use_queue = :queue`, { + queue: true, + }); + + return queryBuilder; + } +} diff --git a/src/modules/item-related/item/infrastructure/dto/filter-item-queue.dto.ts b/src/modules/item-related/item/infrastructure/dto/filter-item-queue.dto.ts new file mode 100644 index 0000000..86f2d47 --- /dev/null +++ b/src/modules/item-related/item/infrastructure/dto/filter-item-queue.dto.ts @@ -0,0 +1,6 @@ +import { BaseFilterDto } from 'src/core/modules/infrastructure/dto/base-filter.dto'; +import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity'; + +export class FilterItemQueueDto + extends BaseFilterDto + implements BaseFilterEntity {} diff --git a/src/modules/item-related/item/infrastructure/dto/item.dto.ts b/src/modules/item-related/item/infrastructure/dto/item.dto.ts index dfdd8dd..8ca6b9b 100644 --- a/src/modules/item-related/item/infrastructure/dto/item.dto.ts +++ b/src/modules/item-related/item/infrastructure/dto/item.dto.ts @@ -134,7 +134,15 @@ export class ItemDto extends BaseStatusDto implements ItemEntity { }, ], }) - @IsArray() + @IsArray({ + message: (body) => { + const value = body.value; + if (!value || value?.length === 0) { + return 'Product bundling tidak boleh kosong.'; + } + return 'Product bundling tidak sesuai.'; + }, + }) @ValidateIf((body) => body.item_type.toLowerCase() == ItemType.BUNDLING) bundling_items: ItemEntity[]; } diff --git a/src/modules/item-related/item/infrastructure/item-read.controller.ts b/src/modules/item-related/item/infrastructure/item-read.controller.ts index a889ac0..91c7ee5 100644 --- a/src/modules/item-related/item/infrastructure/item-read.controller.ts +++ b/src/modules/item-related/item/infrastructure/item-read.controller.ts @@ -6,7 +6,7 @@ import { ItemEntity } from '../domain/entities/item.entity'; import { ItemReadOrchestrator } from '../domain/usecases/item-read.orchestrator'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; -import { Public } from 'src/core/guards'; +import { ExcludePrivilege, Public } from 'src/core/guards'; import { ItemRateEntity } from '../../item-rate/domain/entities/item-rate.entity'; import { FilterItemRateDto } from '../../item-rate/infrastructure/dto/filter-item-rate.dto'; @@ -40,3 +40,19 @@ export class ItemReadController { return await this.orchestrator.indexRate(params); } } + +@ApiTags(`Item Queue - Read`) +@Controller(`v1/item-queue`) +@Public(true) +export class ItemReadQueueController { + constructor(private orchestrator: ItemReadOrchestrator) {} + + @Get() + @Pagination() + @ExcludePrivilege() + async indexQueue( + @Query() params: FilterItemDto, + ): Promise> { + return await this.orchestrator.indexQueue(params); + } +} diff --git a/src/modules/item-related/item/item.module.ts b/src/modules/item-related/item/item.module.ts index bb35be2..76e0fe6 100644 --- a/src/modules/item-related/item/item.module.ts +++ b/src/modules/item-related/item/item.module.ts @@ -4,7 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { ItemDataService } from './data/services/item-data.service'; import { ItemReadService } from './data/services/item-read.service'; -import { ItemReadController } from './infrastructure/item-read.controller'; +import { + ItemReadController, + ItemReadQueueController, +} from './infrastructure/item-read.controller'; import { ItemReadOrchestrator } from './domain/usecases/item-read.orchestrator'; import { ItemDataController } from './infrastructure/item-data.controller'; import { ItemDataOrchestrator } from './domain/usecases/item-data.orchestrator'; @@ -26,6 +29,7 @@ import { ItemRateModel } from '../item-rate/data/models/item-rate.model'; import { ItemRateReadService } from '../item-rate/data/services/item-rate-read.service'; import { IndexItemRatesManager } from './domain/usecases/managers/index-item-rates.manager'; import { UpdateItemRatePriceManager } from './domain/usecases/managers/update-item-rate-price.manager'; +import { IndexItemQueueManager } from './domain/usecases/managers/index-queue-item.manager'; @Global() @Module({ @@ -37,9 +41,14 @@ import { UpdateItemRatePriceManager } from './domain/usecases/managers/update-it ), CqrsModule, ], - controllers: [ItemDataController, ItemReadController], + controllers: [ + ItemDataController, + ItemReadController, + ItemReadQueueController, + ], providers: [ IndexItemManager, + IndexItemQueueManager, IndexItemRatesManager, DetailItemManager, CreateItemManager, @@ -63,6 +72,7 @@ import { UpdateItemRatePriceManager } from './domain/usecases/managers/update-it ], exports: [ IndexItemManager, + IndexItemQueueManager, IndexItemRatesManager, DetailItemManager, CreateItemManager, diff --git a/src/modules/reports/report/report.service.ts b/src/modules/reports/report/report.service.ts index 3613ca1..a6a9e74 100644 --- a/src/modules/reports/report/report.service.ts +++ b/src/modules/reports/report/report.service.ts @@ -99,7 +99,6 @@ export class ReportService extends BaseReportService { const builder = new ReportQueryBuilder(reportConfig, queryModel); const SQL = builder.getSql(); const queryResult = await this.dataSource.query(SQL); - const realData = []; const configColumns = reportConfig.column_configs; diff --git a/src/modules/reports/shared/configs/tenant-report/configs/income-per-item-master.ts b/src/modules/reports/shared/configs/tenant-report/configs/income-per-item-master.ts new file mode 100644 index 0000000..7612dc1 --- /dev/null +++ b/src/modules/reports/shared/configs/tenant-report/configs/income-per-item-master.ts @@ -0,0 +1,10 @@ +import { REPORT_GROUP } from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; +import IncomeReportPerItemMaster from '../../transaction-report/configs/income-per-item-master'; + +export default { + ...IncomeReportPerItemMaster, + group_name: REPORT_GROUP.tenant_report, + unique_name: `${REPORT_GROUP.tenant_report}__income_per_item_master`, + label: 'Pendapatan', +}; diff --git a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts b/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts deleted file mode 100644 index 6718cff..0000000 --- a/src/modules/reports/shared/configs/tenant-report/configs/sample.report.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DATA_FORMAT, DATA_TYPE, REPORT_GROUP } from '../../../constant'; -import { ReportConfigEntity } from '../../../entities/report-config.entity'; - -export default { - group_name: REPORT_GROUP.tenant_report, - unique_name: `${REPORT_GROUP.tenant_report}__sample`, - label: 'Sample Tenant Report', - table_schema: 'season_types main', - main_table_alias: 'main', - defaultOrderBy: [], - lowLevelOrderBy: [], - filter_period_config: { - hidden: true, - }, - column_configs: [ - { - column: 'main__created_at', - query: 'main.created_at', - label: 'Created Date', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_EPOCH, - }, - { - column: 'main__updated_at', - query: 'main.updated_at', - label: 'Updated Date', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_EPOCH, - }, - { - column: 'main__name', - query: 'main.name', - label: 'Name', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.TEXT, - }, - ], - filter_configs: [], -}; diff --git a/src/modules/reports/shared/configs/tenant-report/index.ts b/src/modules/reports/shared/configs/tenant-report/index.ts index ceee528..66e925a 100644 --- a/src/modules/reports/shared/configs/tenant-report/index.ts +++ b/src/modules/reports/shared/configs/tenant-report/index.ts @@ -1,6 +1,6 @@ import { ReportConfigEntity } from '../../entities/report-config.entity'; -import SampleReport from './configs/sample.report'; +import IncomeReportPerItemMaster from './configs/income-per-item-master'; export const TenantReportConfig: ReportConfigEntity[] = [ - // SampleReport + IncomeReportPerItemMaster, ]; diff --git a/src/modules/reports/shared/configs/transaction-report/configs/booking.ts b/src/modules/reports/shared/configs/transaction-report/configs/booking.ts index ffbcab9..c739600 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/booking.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/booking.ts @@ -39,17 +39,16 @@ export default { }, { column: 'main__booking_date', - query: 'main.booking_date', + query: `to_char(main.booking_date, 'DD-MM-YYYY')`, label: 'Tgl. Booking', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__no_of_group', - query: 'main.no_of_group', + query: 'main.no_of_group::NUMERIC', label: '#Visitor', - type: DATA_TYPE.DIMENSION, + type: DATA_TYPE.MEASURE, format: DATA_FORMAT.NUMBER, }, { @@ -111,11 +110,10 @@ export default { { column: 'main__invoice_date', - query: 'main.invoice_date', + query: `to_char(main.invoice_date, 'DD-MM-YYYY')`, label: 'Tgl. Invoice', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__invoice_code', @@ -126,19 +124,17 @@ export default { }, { column: 'main__settlement_date', - query: 'main.settlement_date', + query: `to_char(main.settlement_date, 'DD-MM-YYYY')`, label: 'Tgl Settlement', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'refund__request_date', - query: 'refund.request_date', + query: `to_char(refund.request_date, 'DD-MM-YYYY')`, label: 'Request Refund', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'refund__code', @@ -149,13 +145,11 @@ export default { }, { column: 'refund__refund_date', - query: 'refund.refund_date', + query: `to_char(refund.refund_date, 'DD-MM-YYYY')`, label: 'Tgl. Refund', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, - { column: 'main__creator_name', query: 'main.creator_name', @@ -190,6 +184,7 @@ export default { filter_column: 'main__booking_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Sumber', @@ -231,6 +226,7 @@ export default { filter_column: 'main__invoice_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kode Invoice', @@ -243,12 +239,14 @@ export default { filter_column: 'main__settlement_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Request Refund', filter_column: 'refund__request_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kode Refund', @@ -261,6 +259,7 @@ export default { filter_column: 'refund__refund_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Dibuat Oleh', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/cash-withdrawals.ts b/src/modules/reports/shared/configs/transaction-report/configs/cash-withdrawals.ts index f29b020..a8dd829 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/cash-withdrawals.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/cash-withdrawals.ts @@ -26,14 +26,16 @@ export default { values: [PosLogType.cash_witdrawal], }, ], + whereCondition(filterModel) { + return [`main.created_at is not null`]; + }, column_configs: [ { column: 'main__date', - query: 'main.created_at', + query: `to_char(cast(to_timestamp(main.created_at/1000) as date),'DD-MM-YYYY')`, label: 'Tanggal', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_EPOCH, - date_format: 'DD-MM-YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__time', @@ -64,6 +66,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__pos_name', + query: 'main.pos_name', + label: 'Nama PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__total_balance', query: 'main.total_balance', @@ -77,7 +86,8 @@ export default { filed_label: 'Tanggal', filter_column: 'main__date', field_type: FILTER_FIELD_TYPE.date_range_picker, - filter_type: FILTER_TYPE.DATE_IN_RANGE_EPOCH, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Nama Penarik', @@ -94,8 +104,8 @@ export default { { filed_label: 'No. PoS', filter_column: 'main__pos_number', - field_type: FILTER_FIELD_TYPE.input_tag, - filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, }, ], }; diff --git a/src/modules/reports/shared/configs/transaction-report/configs/cashier-log.ts b/src/modules/reports/shared/configs/transaction-report/configs/cashier-log.ts index c0db8da..8020e49 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/cashier-log.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/cashier-log.ts @@ -26,14 +26,16 @@ export default { values: [PosLogType.login, PosLogType.logout], }, ], + whereCondition(filterModel) { + return [`main.creator_name is not null`]; + }, column_configs: [ { column: 'main__date', - query: 'main.created_at', + query: `to_char(cast(to_timestamp(main.created_at/1000) as date),'DD-MM-YYYY')`, label: 'Tanggal', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_EPOCH, - date_format: 'DD-MM-YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__time', @@ -64,13 +66,21 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__pos_name', + query: 'main.pos_name', + label: 'Nama PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, ], filter_configs: [ { filed_label: 'Tanggal', filter_column: 'main__date', field_type: FILTER_FIELD_TYPE.date_range_picker, - filter_type: FILTER_TYPE.DATE_IN_RANGE_EPOCH, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Tipe', @@ -85,5 +95,11 @@ export default { field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, + { + filed_label: 'No. PoS', + filter_column: 'main__pos_number', + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, + }, ], }; diff --git a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts index 99e1a3e..44bf86e 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts @@ -31,7 +31,17 @@ export default { filter_type: FILTER_TYPE.TEXT_IN_MEMBER, values: [TransactionType.COUNTER], }, + { + column: 'main.is_recap_transaction', + filter_type: FILTER_TYPE.TEXT_EQUAL, + values: [false], + }, ], + whereCondition(filterModel) { + return [ + `main.discount_percentage is not null or main.discount_value is not null`, + ]; + }, defaultOrderBy: [], lowLevelOrderBy: [], filter_period_config: { @@ -40,12 +50,11 @@ export default { column_configs: [ { - column: 'main__settlement_date', - query: 'main.settlement_date', - label: 'Tanggal Transaksi', + column: 'main__payment_date', + query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + label: 'Tgl. Transaksi', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 's_period_type__name', @@ -56,14 +65,21 @@ export default { }, { column: 'main__invoice_code', - query: 'main.invoice_code', - label: 'Kode Transaksi', + query: `CASE WHEN main.type != 'counter' THEN main.invoice_code ELSE null END`, + label: 'Kode Booking', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, { - column: 'main__payment_total_profit', - query: 'main.payment_total_profit', + column: 'main__payment_code', + query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, + label: 'Kode Pembayaran', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_sub_total', + query: 'main.payment_sub_total', label: 'Total Transaksi', type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, @@ -117,6 +133,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__creator_counter_name', + query: 'main.creator_counter_name', + label: 'Nama PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__customer_name', query: 'main.customer_name', @@ -141,10 +164,11 @@ export default { ], filter_configs: [ { - filed_label: 'Tanggal Transaksi', - filter_column: 'main__settlement_date', + filed_label: 'Tgl. Transaksi', + filter_column: 'main__payment_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Tipe Rate', @@ -173,8 +197,8 @@ export default { { filed_label: 'No. Pos', filter_column: 'main__creator_counter_no', - field_type: FILTER_FIELD_TYPE.input_tag, - filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, }, { filed_label: 'Nama Pelanggan', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts new file mode 100644 index 0000000..440c5b3 --- /dev/null +++ b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts @@ -0,0 +1,382 @@ +import { + DATA_FORMAT, + DATA_TYPE, + FILTER_FIELD_TYPE, + FILTER_TYPE, + REPORT_GROUP, +} from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; +import { TransactionType } from 'src/modules/transaction/transaction/constants'; +import { STATUS } from 'src/core/strings/constants/base.constants'; + +export default { + group_name: REPORT_GROUP.transaction_report, + unique_name: `${REPORT_GROUP.transaction_report}__income_per_item_master`, + label: 'Pendapatan Per Item Master', + table_schema: `transactions main + LEFT JOIN transaction_items tr_item ON tr_item.transaction_id::text = main.id::text + LEFT JOIN transaction_item_breakdowns tr_item_bundling ON tr_item_bundling.transaction_item_id::text = tr_item.id::text + LEFT JOIN refunds refund ON refund.transaction_id = main.id + LEFT JOIN items item ON item.id::text = tr_item.item_id::text + LEFT JOIN users tenant ON tenant.id::text = item.tenant_id::text + LEFT JOIN refund_items refund_item ON refund_item.refund_item_id::text = tr_item.item_id::text`, + main_table_alias: 'main', + whereDefaultConditions: [ + { + column: 'main.status', + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + values: [STATUS.SETTLED, STATUS.REFUNDED, STATUS.PROCESS_REFUND], + }, + { + column: 'main.is_recap_transaction', + filter_type: FILTER_TYPE.TEXT_EQUAL, + values: [false], + }, + ], + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + + column_configs: [ + { + column: 'main__payment_date', + query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pendapatan', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__status', + query: 'main.status', + label: 'Status', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.STATUS, + }, + { + column: 'item_owner', + query: `CASE WHEN tenant.name is not null THEN tenant.name ELSE 'Company' END`, + label: 'Kepemilikan', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__type', + query: 'main.type', + label: 'Sumber', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__invoice_code', + query: `CASE WHEN main.type != 'counter' THEN main.invoice_code ELSE null END`, + label: 'Kode Booking', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_code', + query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, + label: 'Kode Pembayaran', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_item__item_category_name', + query: 'tr_item.item_category_name', + label: 'Kategori Item', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_item__item_type', + query: 'tr_item.item_type', + label: 'Tipe Item', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_item_bundling__item_name', + query: `CASE WHEN tr_item.item_type = 'bundling' THEN tr_item.item_name ELSE tr_item_bundling.item_name END`, + label: 'Nama Item Bundling', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_item__item_name', + query: `CASE WHEN tr_item.item_type = 'bundling' THEN tr_item_bundling.item_name ELSE tr_item.item_name END`, + label: 'Nama Item ', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__customer_type', + query: 'main.customer_type', + label: 'Tipe Pelanggan', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__creator_counter_no', + query: 'main.creator_counter_no', + label: 'No.PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__creator_counter_name', + query: 'main.creator_counter_name', + label: 'Nama PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_item__qty', + query: 'tr_item.qty', + label: 'Qty', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.NUMBER, + }, + { + column: 'tr_item_bundling__hpp', + // query: 'tr_item_bundling.hpp', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.total_hpp ELSE tr_item_bundling.hpp END`, + label: 'Total HPP', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__total_price', + // query: 'tr_item_bundling.total_price', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.total_price ELSE tr_item_bundling.total_price END`, + label: 'Subtotal', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__discount_value', + // query: 'tr_item_bundling.discount_value', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.discount_value ELSE tr_item_bundling.discount_value END`, + label: 'Diskon (IDR)', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__total_net_price', + // query: 'tr_item_bundling.total_net_price', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.total_net_price ELSE tr_item_bundling.total_net_price END`, + label: 'Total Penjualan', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__payment_total_dpp', + // query: 'tr_item_bundling.payment_total_dpp', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.payment_total_dpp ELSE tr_item_bundling.payment_total_dpp END`, + label: 'DPP', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__payment_total_tax', + // query: 'tr_item_bundling.payment_total_tax', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.payment_total_tax ELSE tr_item_bundling.payment_total_tax END`, + label: 'Total Pajak', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__total_profit_share', + // query: 'tr_item_bundling.total_profit_share', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.total_profit_share ELSE tr_item_bundling.total_profit_share END`, + label: 'Profit Share', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item_bundling__total_share_tenant', + // query: 'tr_item_bundling.total_share_tenant', + query: `CASE WHEN tr_item.item_type != 'bundling' THEN tr_item.total_share_tenant ELSE tr_item_bundling.total_share_tenant END`, + label: 'Tenant Share', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'refund__refund_date', + query: `to_char(refund.refund_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pengembalian', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'refund__status', + query: 'refund.status', + label: 'Status Pengembalian', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'refund__code', + query: 'refund.code', + label: 'Kode Pengembalian', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'refund_item__qty_refund', + query: 'refund_item.qty_refund', + label: 'Qty Pengembalian', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.NUMBER, + }, + { + column: 'main__customer_name', + query: 'main.customer_name', + label: 'Nama Pelanggan', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__customer_description', + query: 'main.customer_description', + label: 'Deskripsi', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__customer_phone', + query: 'main.customer_phone', + label: 'Telepon', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__creator_name', + query: 'main.creator_name', + label: 'Dibuat Oleh', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + ], + whereCondition(filterModel) { + const queryFilter = []; + const breakdown = filterModel.tr_item__breakdown_bundling; + if (breakdown) { + const value = breakdown.filter.map((item) => { + return item === 'Yes' ? true : false; + }); + + queryFilter.push(`tr_item.breakdown_bundling in (${value.join()})`); + } + return queryFilter; + }, + ignore_filter_keys: ['tr_item__breakdown_bundling'], + filter_configs: [ + { + filed_label: 'Tgl. Pendapatan', + filter_column: 'main__payment_date', + field_type: FILTER_FIELD_TYPE.date_range_picker, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', + }, + { + filed_label: 'Kepemilikan', + filter_column: 'item_owner', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Sumber', + filter_column: 'main__type', + field_type: FILTER_FIELD_TYPE.select, + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + select_custom_options: [...Object.values(TransactionType)], + }, + { + filed_label: 'Kode Booking', + filter_column: 'main__invoice_code', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Kode Pembayaran', + filter_column: 'main__payment_code', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Kategori Item', + filter_column: 'tr_item__item_category_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Nama Item', + filter_column: 'tr_item__item_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Tipe Item', + filter_column: 'tr_item__item_type', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Nama Item Bundling', + filter_column: 'tr_item_bundling__item_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Tipe Pelanggan', + filter_column: 'main__customer_type', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'No. PoS', + filter_column: 'main__creator_counter_no', + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, + }, + { + filed_label: 'Nama PoS', + filter_column: 'main__creator_counter_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Tgl. Pengembalian', + filter_column: 'refund__refund_date', + field_type: FILTER_FIELD_TYPE.date_range_picker, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', + }, + { + filed_label: 'Kode Pengembalian', + filter_column: 'refund__code', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Nama Pelanggan', + filter_column: 'main__customer_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Bank/Issuer', + filter_column: 'main__payment_type_method_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Dibuat Oleh', + filter_column: 'main__creator_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + ], +}; diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts index ef2e131..6b95e10 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts @@ -26,6 +26,11 @@ export default { filter_type: FILTER_TYPE.TEXT_IN_MEMBER, values: [STATUS.SETTLED, STATUS.REFUNDED, STATUS.PROCESS_REFUND], }, + { + column: 'main.is_recap_transaction', + filter_type: FILTER_TYPE.TEXT_EQUAL, + values: [false], + }, ], defaultOrderBy: [], lowLevelOrderBy: [], @@ -35,12 +40,18 @@ export default { column_configs: [ { - column: 'main__settlement_date', - query: 'main.settlement_date', - label: 'Tanggal Pendapatan', + column: 'main__payment_date', + query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pendapatan', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__status', + query: 'main.status', + label: 'Status', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.STATUS, }, { column: 'item_owner', @@ -58,14 +69,14 @@ export default { }, { column: 'main__invoice_code', - query: 'main.invoice_code', + query: `CASE WHEN main.type != 'counter' THEN main.invoice_code ELSE null END`, label: 'Kode Booking', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, { column: 'main__payment_code', - query: 'main.payment_code', + query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, label: 'Kode Pembayaran', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, @@ -77,6 +88,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'tr_item__item_type', + query: 'tr_item.item_type', + label: 'Tipe Item', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'tr_item__item_name', query: 'tr_item.item_name', @@ -98,11 +116,18 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__creator_counter_name', + query: 'main.creator_counter_name', + label: 'Nama PoS', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'tr_item__qty', query: 'tr_item.qty', label: 'Qty', - type: DATA_TYPE.DIMENSION, + type: DATA_TYPE.MEASURE, format: DATA_FORMAT.NUMBER, }, { @@ -112,22 +137,61 @@ export default { type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, }, - // TODO => tambahkan total dpp per item - // TODO => tambahkan total tax { column: 'tr_item__total_price', query: 'tr_item.total_price', + label: 'Subtotal', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item__discount_value', + query: 'tr_item.discount_value', + label: 'Diskon (IDR)', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item__total_net_price', + query: 'tr_item.total_net_price', label: 'Total Penjualan', type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, }, + { + column: 'tr_item__payment_total_dpp', + query: 'tr_item.payment_total_dpp', + label: 'DPP', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item__payment_total_tax', + query: 'tr_item.payment_total_tax', + label: 'Total Pajak', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item__total_profit_share', + query: 'tr_item.total_profit_share', + label: 'Profit Share', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_item__total_share_tenant', + query: 'tr_item.total_share_tenant', + label: 'Tenant Share', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, { column: 'refund__refund_date', - query: 'refund.refund_date', - label: 'Tanggal Pengembalian', + query: `to_char(refund.refund_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pengembalian', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'refund__status', @@ -147,39 +211,9 @@ export default { column: 'refund_item__qty_refund', query: 'refund_item.qty_refund', label: 'Qty Pengembalian', - type: DATA_TYPE.DIMENSION, + type: DATA_TYPE.MEASURE, format: DATA_FORMAT.NUMBER, }, - { - column: 'refund_item__refund_total', - query: '(refund_item.refund_total * -1)', - label: 'Total Pengembalian', - type: DATA_TYPE.MEASURE, - format: DATA_FORMAT.CURRENCY, - }, - - { - column: 'transaction_balance', - query: `CASE WHEN refund.id is null THEN tr_item.total_price ELSE tr_item.total_price - refund_item.refund_total END`, - label: 'Balance', - type: DATA_TYPE.MEASURE, - format: DATA_FORMAT.CURRENCY, - }, - { - column: 'tr_item__item_tenant_share_margin', - query: 'tr_item.item_tenant_share_margin', - label: 'Profile Share (IDR)', - type: DATA_TYPE.MEASURE, - format: DATA_FORMAT.CURRENCY, - }, - { - column: 'tenant_income', - query: 'tr_item.total_price - tr_item.item_tenant_share_margin', - label: 'Pendapatan Tenant', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.CURRENCY, - }, - { column: 'main__customer_name', query: 'main.customer_name', @@ -211,10 +245,11 @@ export default { ], filter_configs: [ { - filed_label: 'Tanggal Pendapatan', - filter_column: 'main__settlement_date', + filed_label: 'Tgl. Pendapatan', + filter_column: 'main__payment_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kepemilikan', @@ -262,14 +297,21 @@ export default { { filed_label: 'No. PoS', filter_column: 'main__creator_counter_no', + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, + }, + { + filed_label: 'Nama PoS', + filter_column: 'main__creator_counter_name', field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, { - filed_label: 'Tanggal Pengembalian', + filed_label: 'Tgl. Pengembalian', filter_column: 'refund__refund_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kode Pengembalian', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income.ts b/src/modules/reports/shared/configs/transaction-report/configs/income.ts index 45f4d5d..85419cd 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/income.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/income.ts @@ -29,6 +29,11 @@ export default { filter_type: FILTER_TYPE.TEXT_IN_MEMBER, values: [STATUS.SETTLED, STATUS.REFUNDED, STATUS.PROCESS_REFUND], }, + { + column: 'main.is_recap_transaction', + filter_type: FILTER_TYPE.TEXT_EQUAL, + values: [false], + }, ], defaultOrderBy: [], lowLevelOrderBy: [], @@ -38,12 +43,18 @@ export default { column_configs: [ { - column: 'main__settlement_date', - query: 'main.settlement_date', - label: 'Tanggal Pendapatan', + column: 'main__payment_date', + query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pendapatan', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__status', + query: 'main.status', + label: 'Status', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.STATUS, }, { column: 'main__type', @@ -61,14 +72,14 @@ export default { }, { column: 'main__invoice_code', - query: 'main.invoice_code', + query: `CASE WHEN main.type != 'counter' THEN main.invoice_code ELSE null END`, label: 'Kode Booking', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, { column: 'main__payment_code', - query: 'main.payment_code', + query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, label: 'Kode Pembayaran', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, @@ -88,12 +99,19 @@ export default { format: DATA_FORMAT.TEXT, }, { - column: 'main__no_of_group', - query: 'main.no_of_group', - label: '#Visitor', + column: 'main__creator_counter_name', + query: 'main.creator_counter_name', + label: 'Nama PoS', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__no_of_group', + query: 'main.no_of_group::NUMERIC', + label: '#Visitor', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.NUMBER, + }, { column: 'item__total_hpp_item', query: 'item.total_hpp_item', @@ -102,9 +120,30 @@ export default { format: DATA_FORMAT.CURRENCY, }, { - column: 'main__reconciliation_mdr', - query: 'main.reconciliation_mdr', - label: 'MDR', + column: 'main__payment_sub_total', + query: 'main.payment_sub_total', + label: 'Subtotal', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'main__discount_percentage', + query: 'main.discount_percentage', + label: 'Diskon (%)', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.NUMBER, + }, + { + column: 'main__discount_value', + query: 'main.discount_value', + label: 'Diskon (IDR)', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'main__payment_total', + query: 'main.payment_total', + label: 'Total Penjualan', type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, }, @@ -123,33 +162,18 @@ export default { format: DATA_FORMAT.CURRENCY, }, { - column: 'main__discount_percentage', - query: 'main.discount_percentage', - label: 'Diskon (%)', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.NUMBER, - }, - { - column: 'main__discount_value', - query: 'main.discount_value', - label: 'Diskon (IDR)', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.CURRENCY, - }, - { - column: 'main__payment_total', - query: 'main.payment_total', - label: 'Total Penjualan', - type: DATA_TYPE.DIMENSION, + column: 'main__payment_total_share', + query: 'main.payment_total_share', + label: 'Profit Share', + type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, }, { column: 'refund__refund_date', - query: 'refund.refund_date', - label: 'Tanggal Pengembalian', + query: `to_char(refund.refund_date, 'DD-MM-YYYY')`, + label: 'Tgl. Pengembalian', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'refund__status', @@ -165,20 +189,20 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, - { - column: 'refund__refund_total', - query: '(refund.refund_total * -1)', - label: 'Total Pengembalian', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.CURRENCY, - }, - { - column: 'transaction_balance', - query: `CASE WHEN refund.id is null THEN main.payment_total ELSE main.payment_total - refund.refund_total END`, - label: 'Balance', - type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.CURRENCY, - }, + // { + // column: 'refund__refund_total', + // query: '(refund.refund_total * -1)', + // label: 'Total Pengembalian', + // type: DATA_TYPE.MEASURE, + // format: DATA_FORMAT.CURRENCY, + // }, + // { + // column: 'transaction_balance', + // query: `CASE WHEN refund.id is null THEN main.payment_total ELSE main.payment_total - refund.refund_total END`, + // label: 'Balance', + // type: DATA_TYPE.MEASURE, + // format: DATA_FORMAT.CURRENCY, + // }, { column: 'main__discount_code', query: 'main.discount_code', @@ -252,11 +276,13 @@ export default { ], filter_configs: [ { - filed_label: 'Tanggal Pendapatan', - filter_column: 'main__settlement_date', + filed_label: 'Tgl. Pendapatan', + filter_column: 'main__payment_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, + { filed_label: 'Sumber', filter_column: 'main__type', @@ -291,14 +317,21 @@ export default { { filed_label: 'No. PoS', filter_column: 'main__creator_counter_no', + field_type: FILTER_FIELD_TYPE.input_number, + filter_type: FILTER_TYPE.NUMBER_EQUAL, + }, + { + filed_label: 'Nama PoS', + filter_column: 'main__creator_counter_name', field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, { - filed_label: 'Tanggal Pengembalian', + filed_label: 'Tgl. Pengembalian', filter_column: 'refund__refund_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kode Pengembalian', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts index ed553c0..6478d1b 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts @@ -37,20 +37,25 @@ export default { format: DATA_FORMAT.TEXT, }, { - column: 'main__settlement_date', - query: 'main.settlement_date', - label: 'Tgl. Pendapatan', + column: 'main__payment_date', + query: `CASE WHEN main.payment_date is not null THEN to_char(main.payment_date, 'DD-MM-YYYY') ELSE null END`, + label: 'Tgl. Transaksi', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_date_bank', + query: `CASE WHEN main.payment_date_bank is not null THEN to_char(main.payment_date_bank, 'DD-MM-YYYY') ELSE null END`, + label: 'Tgl. Transaksi Bank', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, }, { column: 'main__reconciliation_confirm_date', - query: 'main.reconciliation_confirm_date', + query: `CASE WHEN main.reconciliation_confirm_date is not null THEN to_char(cast(to_timestamp(COALESCE(main.reconciliation_confirm_date::NUMERIC)/1000) as date),'DD-MM-YYYY') ELSE null END`, label: 'Tgl. Konfirmasi', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__payment_code_reference', @@ -94,6 +99,13 @@ export default { type: DATA_TYPE.MEASURE, format: DATA_FORMAT.CURRENCY, }, + { + column: 'main__payment_total_net_profit', + query: 'main.payment_total_net_profit', + label: 'Net Pendapatan', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, { column: 'cashier', query: `CASE WHEN main.type = 'counter' THEN main.creator_name END`, @@ -129,16 +141,25 @@ export default { select_custom_options: [...Object.values(TransactionType)], }, { - filed_label: 'Tgl. Pendapatan', - filter_column: 'main__settlement_date', + filed_label: 'Tgl. Transaksi', + filter_column: 'main__payment_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', + }, + { + filed_label: 'Tgl. Transaksi Bank', + filter_column: 'main__payment_date_bank', + field_type: FILTER_FIELD_TYPE.date_range_picker, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Tgl. Konfirmasi', filter_column: 'main__reconciliation_confirm_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Referensi', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/refunds.ts b/src/modules/reports/shared/configs/transaction-report/configs/refunds.ts index d0b2322..252f0c8 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/refunds.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/refunds.ts @@ -46,19 +46,17 @@ export default { }, { column: 'main__request_date', - query: 'main.request_date', + query: `to_char(main.request_date, 'DD-MM-YYYY')`, label: 'Tgl. Permintaan', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'main__refund_date', - query: 'main.refund_date', + query: `to_char(main.refund_date, 'DD-MM-YYYY')`, label: 'Tgl. Refund', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_TIMESTAMP, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'tr__payment_code', @@ -69,11 +67,10 @@ export default { }, { column: 'tr__settlement_date', - query: 'tr.settlement_date', + query: `to_char(tr.settlement_date, 'DD-MM-YYYY')`, label: 'Tgl. Settlement', type: DATA_TYPE.DIMENSION, - format: DATA_FORMAT.DATE_EPOCH, - date_format: 'DD/MM/YYYY', + format: DATA_FORMAT.TEXT, }, { column: 'tr__payment_total', @@ -185,12 +182,14 @@ export default { filter_column: 'main__request_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Tgl. Refund', filter_column: 'main__refund_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Kode Settlement', @@ -203,6 +202,7 @@ export default { filter_column: 'tr__settlement_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', }, { filed_label: 'Bank Tujuan', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/tax.ts b/src/modules/reports/shared/configs/transaction-report/configs/tax.ts new file mode 100644 index 0000000..ed097fb --- /dev/null +++ b/src/modules/reports/shared/configs/transaction-report/configs/tax.ts @@ -0,0 +1,136 @@ +import { + DATA_FORMAT, + DATA_TYPE, + FILTER_FIELD_TYPE, + FILTER_TYPE, + REPORT_GROUP, +} from '../../../constant'; +import { ReportConfigEntity } from '../../../entities/report-config.entity'; +import { TransactionType } from 'src/modules/transaction/transaction/constants'; +import { STATUS } from 'src/core/strings/constants/base.constants'; + +export default { + group_name: REPORT_GROUP.transaction_report, + unique_name: `${REPORT_GROUP.transaction_report}__tax`, + label: 'Tax Pendapatan', + table_schema: `transactions main + left join transaction_taxes tr_taxes on tr_taxes.transaction_id = main.id`, + main_table_alias: 'main', + whereDefaultConditions: [ + { + column: 'main.status', + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + values: [STATUS.SETTLED, STATUS.REFUNDED, STATUS.PROCESS_REFUND], + }, + { + column: 'main.is_recap_transaction', + filter_type: FILTER_TYPE.TEXT_EQUAL, + values: [false], + }, + ], + defaultOrderBy: [], + lowLevelOrderBy: [], + filter_period_config: { + hidden: true, + }, + + column_configs: [ + { + column: 'main__payment_date', + query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + label: 'Tgl. Transaksi', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__status', + query: 'main.status', + label: 'Status', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.STATUS, + }, + { + column: 'main__type', + query: 'main.type', + label: 'Sumber', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__invoice_code', + query: `CASE WHEN main.type != 'counter' THEN main.invoice_code ELSE null END`, + label: 'Kode Booking', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_code', + query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, + label: 'Kode Pembayaran', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_total', + query: 'main.payment_total', + label: 'Total Penjualan', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'main__payment_total_dpp', + query: 'main.payment_total_dpp', + label: 'DPP', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + { + column: 'tr_taxes__tax_name', + query: 'tr_taxes.tax_name', + label: 'Tipe Tax', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'tr_taxes__tax_total_value', + query: 'tr_taxes.tax_total_value', + label: 'Value Tax', + type: DATA_TYPE.MEASURE, + format: DATA_FORMAT.CURRENCY, + }, + ], + filter_configs: [ + { + filed_label: 'Tgl. Transaksi', + filter_column: 'main__payment_date', + field_type: FILTER_FIELD_TYPE.date_range_picker, + filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + date_format: 'DD-MM-YYYY', + }, + { + filed_label: 'Sumber', + filter_column: 'main__type', + field_type: FILTER_FIELD_TYPE.select, + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + select_custom_options: [...Object.values(TransactionType)], + }, + { + filed_label: 'Kode Booking', + filter_column: 'main__invoice_code', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Kode Pembayaran', + filter_column: 'main__payment_code', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + { + filed_label: 'Tipe Tax', + filter_column: 'tr_taxes__tax_name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, + ], +}; diff --git a/src/modules/reports/shared/configs/transaction-report/index.ts b/src/modules/reports/shared/configs/transaction-report/index.ts index 1f3b73a..644760d 100644 --- a/src/modules/reports/shared/configs/transaction-report/index.ts +++ b/src/modules/reports/shared/configs/transaction-report/index.ts @@ -2,6 +2,7 @@ import { ReportConfigEntity } from '../../entities/report-config.entity'; import IncomeReport from './configs/income'; import IncomeReportPerItem from './configs/income-per-item'; +import IncomeReportPerItemMaster from './configs/income-per-item-master'; import GivingDiscount from './configs/giving-discounts'; import VisitorsPerRideReport from './configs/visitors-per-ride'; import TimePerRideReport from './configs/time-per-ride'; @@ -10,10 +11,12 @@ import RefundsReport from './configs/refunds'; import CashierLogReport from './configs/cashier-log'; import CashWithdrawalsReport from './configs/cash-withdrawals'; import ReconciliationReport from './configs/reconciliation'; +import TaxReport from './configs/tax'; export const TransactionReportConfig: ReportConfigEntity[] = [ IncomeReport, IncomeReportPerItem, + IncomeReportPerItemMaster, GivingDiscount, // VisitorsPerRideReport, // TimePerRideReport, @@ -22,4 +25,5 @@ export const TransactionReportConfig: ReportConfigEntity[] = [ CashierLogReport, CashWithdrawalsReport, ReconciliationReport, + TaxReport, ]; diff --git a/src/modules/reports/shared/entities/report-config.entity.ts b/src/modules/reports/shared/entities/report-config.entity.ts index 71598b8..fe563d4 100644 --- a/src/modules/reports/shared/entities/report-config.entity.ts +++ b/src/modules/reports/shared/entities/report-config.entity.ts @@ -12,7 +12,7 @@ export interface ReportColumnConfigEntity { export interface FilterConfigEntity { filter_column: string; filter_type: FILTER_TYPE; - + date_format?: string; filed_label: string; field_type: string; hide_field?: boolean; @@ -46,7 +46,7 @@ export interface ReportConfigEntity { whereDefaultConditions?: { column: string; filter_type: FILTER_TYPE; - values: string[]; + values: string[] | boolean[] | number[]; }[]; defaultOrderBy?: string[]; lowLevelOrderBy?: string[]; diff --git a/src/modules/reports/shared/helpers/query-builder.ts b/src/modules/reports/shared/helpers/query-builder.ts index 6c58e96..99aab40 100644 --- a/src/modules/reports/shared/helpers/query-builder.ts +++ b/src/modules/reports/shared/helpers/query-builder.ts @@ -331,7 +331,11 @@ export class ReportQueryBuilder { groupKeys.forEach(function (key, index) { const colName = rowGroupCols[index].field; // whereParts.push(colName + ' = "' + key + '"'); - whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`); + if (!key) { + whereParts.push(`${thisSelf.findQueryConfig(colName)} is null`); + } else { + whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`); + } }); } @@ -363,6 +367,7 @@ export class ReportQueryBuilder { const tableWhereConditions = [...whereCondition, ...whereParts].filter( Boolean, ); + const defaultWhereConditions = defaultWhereOptions.filter(Boolean); if (tableWhereConditions.length > 0) { diff --git a/src/modules/transaction/profit-share-formula/domain/usecases/managers/index-profit-share-formula.manager.ts b/src/modules/transaction/profit-share-formula/domain/usecases/managers/index-profit-share-formula.manager.ts new file mode 100644 index 0000000..8936608 --- /dev/null +++ b/src/modules/transaction/profit-share-formula/domain/usecases/managers/index-profit-share-formula.manager.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { + Param, + RelationParam, +} from 'src/core/modules/domain/entities/base-filter.entity'; +import { SalesPriceFormulaEntity } from 'src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity'; +import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager'; +import { SelectQueryBuilder } from 'typeorm'; + +@Injectable() +export class IndexProfitShareFormulaManager extends BaseIndexManager { + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + return queryBuilder; + } + + get specificFilter(): Param[] { + return []; + } + + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + // relation only join (for query purpose) + joinRelations: [], + + // relation join and select (relasi yang ingin ditampilkan), + selectRelations: [], + + // relation yang hanya ingin dihitung (akan return number) + countRelations: [], + }; + } + + get selects(): string[] { + // return []; + return [ + `${this.tableName}.id`, + `${this.tableName}.formula_render`, + `${this.tableName}.formula_string`, + `${this.tableName}.value_for`, + ]; + } +} diff --git a/src/modules/transaction/profit-share-formula/domain/usecases/managers/tax-formula.manager.ts b/src/modules/transaction/profit-share-formula/domain/usecases/managers/tax-formula.manager.ts new file mode 100644 index 0000000..acfcd14 --- /dev/null +++ b/src/modules/transaction/profit-share-formula/domain/usecases/managers/tax-formula.manager.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { + Param, + RelationParam, +} from 'src/core/modules/domain/entities/base-filter.entity'; +import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager'; +import { SelectQueryBuilder } from 'typeorm'; +import { TaxEntity } from 'src/modules/transaction/tax/domain/entities/tax.entity'; + +@Injectable() +export class IndexTaxFormulaManager extends BaseIndexManager { + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + return queryBuilder; + } + + get specificFilter(): Param[] { + return [ + { + cols: `${this.tableName}.status::text`, + isStatus: true, + data: [`'active'`], + }, + ]; + } + + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + // relation only join (for query purpose) + joinRelations: [], + + // relation join and select (relasi yang ingin ditampilkan), + selectRelations: [], + + // relation yang hanya ingin dihitung (akan return number) + countRelations: [], + }; + } + + get selects(): string[] { + // return []; + return [ + `${this.tableName}.id`, + `${this.tableName}.formula_render`, + `${this.tableName}.formula_string`, + `${this.tableName}.name`, + ]; + } +} diff --git a/src/modules/transaction/profit-share-formula/domain/usecases/managers/update-profit-share-formula.manager.ts b/src/modules/transaction/profit-share-formula/domain/usecases/managers/update-profit-share-formula.manager.ts index 053114a..52da542 100644 --- a/src/modules/transaction/profit-share-formula/domain/usecases/managers/update-profit-share-formula.manager.ts +++ b/src/modules/transaction/profit-share-formula/domain/usecases/managers/update-profit-share-formula.manager.ts @@ -8,20 +8,21 @@ import { import { SalesPriceFormulaModel } from 'src/modules/transaction/sales-price-formula/data/models/sales-price-formula.model'; import { ProfitShareFormulaUpdatedEvent } from '../../entities/event/profit-share-formula-updated.event'; import { SalesPriceFormulaEntity } from 'src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity'; -import { In } from 'typeorm'; -import { STATUS } from 'src/core/strings/constants/base.constants'; -import { calculateProfitFormula } from 'src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper'; +// import { In } from 'typeorm'; +// import { STATUS } from 'src/core/strings/constants/base.constants'; +// import { calculateProfitFormula } from 'src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper'; @Injectable() export class UpdateProfitShareFormulaManager extends BaseUpdateManager { async validateProcess(): Promise { - const taxes = await this.dataServiceFirstOpt.getManyByOptions({ - where: { - status: In([STATUS.ACTIVE]), - }, - }); + // const taxes = await this.dataServiceFirstOpt.getManyByOptions({ + // where: { + // status: In([STATUS.ACTIVE]), + // }, + // }); - calculateProfitFormula(this.data.formula_string, taxes, 10000, 50, true); + // TODO: Save Validation + // calculateProfitFormula(this.data.formula_string, taxes, 10000, 50, true); return; } @@ -30,6 +31,10 @@ export class UpdateProfitShareFormulaManager extends BaseUpdateManager { + const additionalFormula = this.data.additional; + const taxFormula = this.data.taxes; + this.dataService.create(null, null, additionalFormula); + this.dataServiceFirstOpt.create(null, null, taxFormula); return; } diff --git a/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-data.orchestrator.ts b/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-data.orchestrator.ts index a1d68df..0bf359d 100644 --- a/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-data.orchestrator.ts +++ b/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-data.orchestrator.ts @@ -17,7 +17,7 @@ export class ProfitShareFormulaDataOrchestrator { async update(data): Promise { const formula = await this.serviceData.getOneByOptions({ where: { - type: FormulaType.PROFIT_SHARE, + type: FormulaType.SALES_PRICE, }, }); diff --git a/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-read.orchestrator.ts b/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-read.orchestrator.ts index ef86f67..0659b8e 100644 --- a/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-read.orchestrator.ts +++ b/src/modules/transaction/profit-share-formula/domain/usecases/profit-share-formula-read.orchestrator.ts @@ -3,14 +3,41 @@ import { DetailProfitShareFormulaManager } from './managers/detail-profit-share- import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { SalesPriceFormulaReadService } from 'src/modules/transaction/sales-price-formula/data/services/sales-price-formula-read.service'; import { SalesPriceFormulaEntity } from 'src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity'; +import { IndexProfitShareFormulaManager } from './managers/index-profit-share-formula.manager'; +import { IndexTaxFormulaManager } from './managers/tax-formula.manager'; +import { TaxReadService } from 'src/modules/transaction/tax/data/services/tax-read.service'; @Injectable() export class ProfitShareFormulaReadOrchestrator { constructor( private detailManager: DetailProfitShareFormulaManager, + private indexManager: IndexProfitShareFormulaManager, + private taxManager: IndexTaxFormulaManager, private serviceData: SalesPriceFormulaReadService, + private taxServiceData: TaxReadService, ) {} + async index(): Promise { + this.indexManager.setFilterParam({}); + this.indexManager.setService(this.serviceData, TABLE_NAME.PRICE_FORMULA); + await this.indexManager.execute(); + const { data } = this.indexManager.getResult(); + return data; + } + + async tax(): Promise[]> { + this.taxManager.setFilterParam({}); + this.taxManager.setService(this.taxServiceData, TABLE_NAME.TAX); + await this.taxManager.execute(); + const { data } = this.taxManager.getResult(); + return data.map((tax) => { + return { + ...tax, + value_for: tax.name, + }; + }); + } + async detail(): Promise { this.detailManager.setData(''); this.detailManager.setService(this.serviceData, TABLE_NAME.PRICE_FORMULA); diff --git a/src/modules/transaction/profit-share-formula/infrastructure/profit-share-formula-read.controller.ts b/src/modules/transaction/profit-share-formula/infrastructure/profit-share-formula-read.controller.ts index 7a12ffe..c98f929 100644 --- a/src/modules/transaction/profit-share-formula/infrastructure/profit-share-formula-read.controller.ts +++ b/src/modules/transaction/profit-share-formula/infrastructure/profit-share-formula-read.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ProfitShareFormulaReadOrchestrator } from '../domain/usecases/profit-share-formula-read.orchestrator'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; @@ -15,4 +15,14 @@ export class ProfitShareFormulaReadController { async detail(): Promise { return await this.orchestrator.detail(); } + + @Get('detail') + async breakdown(): Promise { + return await this.orchestrator.index(); + } + + @Get('tax') + async tax(): Promise[]> { + return await this.orchestrator.tax(); + } } diff --git a/src/modules/transaction/profit-share-formula/profit-share-formula.module.ts b/src/modules/transaction/profit-share-formula/profit-share-formula.module.ts index 31ff6f2..4637dd4 100644 --- a/src/modules/transaction/profit-share-formula/profit-share-formula.module.ts +++ b/src/modules/transaction/profit-share-formula/profit-share-formula.module.ts @@ -12,6 +12,9 @@ import { DetailProfitShareFormulaManager } from './domain/usecases/managers/deta import { SalesPriceFormulaModel } from '../sales-price-formula/data/models/sales-price-formula.model'; import { TaxDataService } from '../tax/data/services/tax-data.service'; import { TaxModel } from '../tax/data/models/tax.model'; +import { IndexProfitShareFormulaManager } from './domain/usecases/managers/index-profit-share-formula.manager'; +import { IndexTaxFormulaManager } from './domain/usecases/managers/tax-formula.manager'; +import { TaxReadService } from '../tax/data/services/tax-read.service'; @Module({ imports: [ @@ -29,11 +32,14 @@ import { TaxModel } from '../tax/data/models/tax.model'; providers: [ DetailProfitShareFormulaManager, UpdateProfitShareFormulaManager, + IndexProfitShareFormulaManager, + IndexTaxFormulaManager, ProfitShareFormulaDataOrchestrator, ProfitShareFormulaReadOrchestrator, TaxDataService, + TaxReadService, ], }) export class ProfitShareFormulaModule {} diff --git a/src/modules/transaction/reconciliation/domain/usecases/managers/batch-confirm-reconciliation.manager.ts b/src/modules/transaction/reconciliation/domain/usecases/managers/batch-confirm-reconciliation.manager.ts index e9c8347..218a556 100644 --- a/src/modules/transaction/reconciliation/domain/usecases/managers/batch-confirm-reconciliation.manager.ts +++ b/src/modules/transaction/reconciliation/domain/usecases/managers/batch-confirm-reconciliation.manager.ts @@ -20,6 +20,7 @@ export class BatchConfirmReconciliationManager extends BaseBatchUpdateStatusMana status: STATUS.SETTLED, reconciliation_status: this.dataStatus, payment_code: await generateInvoiceCodeHelper(this.dataService, 'PMY'), + payment_date_bank: data.payment_date_bank ?? new Date(), }); return; } diff --git a/src/modules/transaction/reconciliation/domain/usecases/managers/confirm-reconciliation.manager.ts b/src/modules/transaction/reconciliation/domain/usecases/managers/confirm-reconciliation.manager.ts index 213341f..f2fd309 100644 --- a/src/modules/transaction/reconciliation/domain/usecases/managers/confirm-reconciliation.manager.ts +++ b/src/modules/transaction/reconciliation/domain/usecases/managers/confirm-reconciliation.manager.ts @@ -31,6 +31,7 @@ export class ConfirmReconciliationManager extends BaseUpdateStatusManager { constructor( @InjectRepository(SalesPriceFormulaModel, CONNECTION_NAME.DEFAULT) private repo: Repository, + @InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT) + private tax: Repository, + @InjectRepository(ItemModel, CONNECTION_NAME.DEFAULT) + private item: Repository, ) { super(repo); } + + profitShareFormula() { + return this.repo.find({ + where: { + type: FormulaType.PROFIT_SHARE, + }, + }); + } + + async itemTax(id: string) { + const item = await this.item.findOne({ + relations: ['tenant'], + where: { + id, + }, + }); + + const profitShare = (item?.share_profit ?? 0) / 100; + const tenantShare = (item?.tenant?.share_margin ?? 0) / 100; + + return { profitShare, tenantShare }; + } + + async salesPriceFormula() { + const salesFormula = await this.repo.findOne({ + where: { + type: FormulaType.SALES_PRICE, + }, + }); + + const taxes = await this.tax.find(); + + for (const tax of taxes) { + salesFormula.formula_string = salesFormula.formula_string.replace( + tax.name, + `(${tax.formula_string})`, + ); + } + + const counter = {}; + do { + let isInfinite = false; + for (const tax of taxes) { + salesFormula.formula_string = salesFormula.formula_string.replace( + `${tax.name}_value`, + `(${tax.formula_string})`, + ); + + if (salesFormula.formula_string.includes(`${tax.name}_value`)) + counter[tax.name] = counter[tax.name] ? counter[tax.name] + 1 : 1; + + for (const count of Object.keys(counter)) { + if (!isInfinite && counter[count] > 50) isInfinite = true; + } + } + + if (isInfinite) { + throw new UnprocessableEntityException({ + message: 'Infinity Loop Formula, please fix formula', + error: 'Infinity Loop Formula', + meta: counter, + }); + } + } while (salesFormula.formula_string.includes('_value')); + + return salesFormula; + } } diff --git a/src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity.ts b/src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity.ts index c0adb62..aa57e47 100644 --- a/src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity.ts +++ b/src/modules/transaction/sales-price-formula/domain/entities/sales-price-formula.entity.ts @@ -6,5 +6,13 @@ export interface SalesPriceFormulaEntity extends BaseEntity { formula_string: string; // digunakan untuk menyimpan string dari formula example_formula: string; example_result: number; + value_for: string; type: FormulaType; + additional?: AdditionalFormula[]; +} + +export interface AdditionalFormula { + formula_render: any; + formula_string: string; + value_for: string; } diff --git a/src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper.ts b/src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper.ts index f83e7b0..aff347c 100644 --- a/src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper.ts +++ b/src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper.ts @@ -1,6 +1,46 @@ import * as math from 'mathjs'; import { Equation, parse } from 'algebra.js'; -import { HttpStatus, UnprocessableEntityException } from '@nestjs/common'; +import { + BadRequestException, + HttpStatus, + UnprocessableEntityException, +} from '@nestjs/common'; +import { apm } from 'src/core/apm'; + +export function calculateFormula( + formula: string, + variable: object, + total: number, + solveFor = 'dpp', +) { + try { + const x1 = math.simplify(formula, variable).toString(); + // console.log('Formula ', x1); + const dppFormula = parse(x1); + const totalFormula = parse(total.toString()); + const equation = new Equation(totalFormula, dppFormula); + + // console.log(equation.toString()); + const result = equation.solveFor(solveFor).toString(); + // console.log(result, 'formula'); + + const value = math.evaluate(result); + // console.log(value, 'value'); + + return value; + } catch (e) { + apm.captureError(e); + throw new BadRequestException({ + message: 'Wrong value', + meta: { + formula, + variable, + total, + solveFor, + }, + }); + } +} export function calculateSalesFormula( formula: string, diff --git a/src/modules/transaction/sales-price-formula/infrastructure/dto/sales-price-formula.dto.ts b/src/modules/transaction/sales-price-formula/infrastructure/dto/sales-price-formula.dto.ts index 4cc7542..6fab1d8 100644 --- a/src/modules/transaction/sales-price-formula/infrastructure/dto/sales-price-formula.dto.ts +++ b/src/modules/transaction/sales-price-formula/infrastructure/dto/sales-price-formula.dto.ts @@ -1,11 +1,37 @@ import { BaseDto } from 'src/core/modules/infrastructure/dto/base.dto'; -import { SalesPriceFormulaEntity } from '../../domain/entities/sales-price-formula.entity'; +import { + AdditionalFormula, + SalesPriceFormulaEntity, +} from '../../domain/entities/sales-price-formula.entity'; import { ApiProperty } from '@nestjs/swagger'; -import { ValidateIf } from 'class-validator'; +import { ValidateIf, ValidateNested } from 'class-validator'; import { Exclude } from 'class-transformer'; import { Any } from 'typeorm'; import { FormulaType } from '../../constants'; +export class AdditionalFormulaDto implements AdditionalFormula { + @ApiProperty({ + type: Any, + required: false, + }) + @ValidateIf((body) => body.formula_render) + formula_render: any; + + @ApiProperty({ + type: String, + required: false, + }) + @ValidateIf((body) => body.formula_string) + formula_string: string; + + @ApiProperty({ + type: String, + required: false, + }) + @ValidateIf((body) => body.value_for) + value_for: string; +} + export class SalesPriceFormulaDto extends BaseDto implements SalesPriceFormulaEntity @@ -30,6 +56,19 @@ export class SalesPriceFormulaDto @Exclude() example_result: number; + @Exclude() + value_for: string; + @Exclude() type: FormulaType; + + @ApiProperty({ + type: [AdditionalFormulaDto], + default: AdditionalFormulaDto, + }) + @ValidateIf(({ additional }) => { + return additional != null; + }) + @ValidateNested({ each: true }) + additional: AdditionalFormulaDto[]; } diff --git a/src/modules/transaction/sales-price-formula/sales-price-formula.module.ts b/src/modules/transaction/sales-price-formula/sales-price-formula.module.ts index accd83c..02be135 100644 --- a/src/modules/transaction/sales-price-formula/sales-price-formula.module.ts +++ b/src/modules/transaction/sales-price-formula/sales-price-formula.module.ts @@ -14,13 +14,14 @@ import { DetailSalesPriceFormulaManager } from './domain/usecases/managers/detai import { SalesPriceFormulaModel } from './data/models/sales-price-formula.model'; import { TaxDataService } from '../tax/data/services/tax-data.service'; import { TaxModel } from '../tax/data/models/tax.model'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; @Global() @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature( - [SalesPriceFormulaModel, TaxModel], + [SalesPriceFormulaModel, TaxModel, ItemModel], CONNECTION_NAME.DEFAULT, ), CqrsModule, diff --git a/src/modules/transaction/tax/data/models/tax.model.ts b/src/modules/transaction/tax/data/models/tax.model.ts index cad7509..108d4f2 100644 --- a/src/modules/transaction/tax/data/models/tax.model.ts +++ b/src/modules/transaction/tax/data/models/tax.model.ts @@ -10,4 +10,10 @@ export class TaxModel extends BaseStatusModel implements TaxEntity { @Column('float', { name: 'value', default: 0 }) value: number; + + @Column('json', { name: 'formula_render', nullable: true }) + formula_render: any; + + @Column('varchar', { name: 'formula_string', nullable: true }) + formula_string: string; } diff --git a/src/modules/transaction/tax/data/services/tax-data.service.ts b/src/modules/transaction/tax/data/services/tax-data.service.ts index 87d4967..7b5d70c 100644 --- a/src/modules/transaction/tax/data/services/tax-data.service.ts +++ b/src/modules/transaction/tax/data/services/tax-data.service.ts @@ -3,7 +3,10 @@ import { BaseDataService } from 'src/core/modules/data/service/base-data.service import { TaxEntity } from '../../domain/entities/tax.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { TaxModel } from '../models/tax.model'; -import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { + CONNECTION_NAME, + STATUS, +} from 'src/core/strings/constants/base.constants'; import { Repository } from 'typeorm'; @Injectable() @@ -14,4 +17,21 @@ export class TaxDataService extends BaseDataService { ) { super(repo); } + + async taxKeyValue(): Promise { + const taxes = await this.getManyByOptions({ + where: { + status: STATUS.ACTIVE, + }, + }); + + const keyVal = {}; + + for (const tax of taxes) { + const { name, value } = tax; + keyVal[name] = value / 100; + } + + return keyVal; + } } diff --git a/src/modules/transaction/tax/domain/entities/tax.entity.ts b/src/modules/transaction/tax/domain/entities/tax.entity.ts index 9873d88..12bdf95 100644 --- a/src/modules/transaction/tax/domain/entities/tax.entity.ts +++ b/src/modules/transaction/tax/domain/entities/tax.entity.ts @@ -3,4 +3,5 @@ import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.e export interface TaxEntity extends BaseStatusEntity { name: string; value: number; + formula_string?: string; } diff --git a/src/modules/transaction/tax/domain/usecases/managers/index-tax-formula.manager.ts b/src/modules/transaction/tax/domain/usecases/managers/index-tax-formula.manager.ts new file mode 100644 index 0000000..642c3fa --- /dev/null +++ b/src/modules/transaction/tax/domain/usecases/managers/index-tax-formula.manager.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { IndexTaxManager } from './index-tax.manager'; +import { Param } from 'src/core/modules/domain/entities/base-filter.entity'; + +@Injectable() +export class IndexTaxFormulaManager extends IndexTaxManager { + get selects(): string[] { + return [ + `${this.tableName}.id`, + `${this.tableName}.name`, + `${this.tableName}.value`, + ]; + } + + get specificFilter(): Param[] { + return [ + { + cols: `${this.tableName}.status::text`, + data: [`'active'`], + isStatus: true, + }, + ...super.specificFilter, + ]; + } +} diff --git a/src/modules/transaction/tax/domain/usecases/tax-read.orchestrator.ts b/src/modules/transaction/tax/domain/usecases/tax-read.orchestrator.ts index 80a0297..059eeac 100644 --- a/src/modules/transaction/tax/domain/usecases/tax-read.orchestrator.ts +++ b/src/modules/transaction/tax/domain/usecases/tax-read.orchestrator.ts @@ -6,11 +6,13 @@ import { PaginationResponse } from 'src/core/response/domain/ok-response.interfa import { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-read.orchestrator'; import { DetailTaxManager } from './managers/detail-tax.manager'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { IndexTaxFormulaManager } from './managers/index-tax-formula.manager'; @Injectable() export class TaxReadOrchestrator extends BaseReadOrchestrator { constructor( private indexManager: IndexTaxManager, + private formulaManager: IndexTaxFormulaManager, private detailManager: DetailTaxManager, private serviceData: TaxReadService, ) { @@ -24,6 +26,13 @@ export class TaxReadOrchestrator extends BaseReadOrchestrator { return this.indexManager.getResult(); } + async formula(params): Promise> { + this.formulaManager.setFilterParam(params); + this.formulaManager.setService(this.serviceData, TABLE_NAME.TAX); + await this.formulaManager.execute(); + return this.formulaManager.getResult(); + } + async detail(dataId: string): Promise { this.detailManager.setData(dataId); this.detailManager.setService(this.serviceData, TABLE_NAME.TAX); diff --git a/src/modules/transaction/tax/infrastructure/tax-read.controller.ts b/src/modules/transaction/tax/infrastructure/tax-read.controller.ts index 773f2a0..b1c6ae2 100644 --- a/src/modules/transaction/tax/infrastructure/tax-read.controller.ts +++ b/src/modules/transaction/tax/infrastructure/tax-read.controller.ts @@ -23,6 +23,14 @@ export class TaxReadController { return await this.orchestrator.index(params); } + @Get('formula') + @Pagination() + async formula( + @Query() params: FilterTaxDto, + ): Promise> { + return await this.orchestrator.formula(params); + } + @Get(':id') async detail(@Param('id') id: string): Promise { return await this.orchestrator.detail(id); diff --git a/src/modules/transaction/tax/tax.module.ts b/src/modules/transaction/tax/tax.module.ts index 46682b4..97be019 100644 --- a/src/modules/transaction/tax/tax.module.ts +++ b/src/modules/transaction/tax/tax.module.ts @@ -24,6 +24,7 @@ import { BatchInactiveTaxManager } from './domain/usecases/managers/batch-inacti import { TaxModel } from './data/models/tax.model'; import { SalesPriceFormulaReadService } from '../sales-price-formula/data/services/sales-price-formula-read.service'; import { SalesPriceFormulaModel } from '../sales-price-formula/data/models/sales-price-formula.model'; +import { IndexTaxFormulaManager } from './domain/usecases/managers/index-tax-formula.manager'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { SalesPriceFormulaModel } from '../sales-price-formula/data/models/sales controllers: [TaxDataController, TaxReadController], providers: [ IndexTaxManager, + IndexTaxFormulaManager, DetailTaxManager, CreateTaxManager, DeleteTaxManager, diff --git a/src/modules/transaction/transaction/data/models/transaction-item.model.ts b/src/modules/transaction/transaction/data/models/transaction-item.model.ts index 01eab5a..034bb47 100644 --- a/src/modules/transaction/transaction/data/models/transaction-item.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction-item.model.ts @@ -7,6 +7,7 @@ import { } from '../../domain/entities/transaction-item.entity'; import { TransactionModel } from './transaction.model'; import { RefundItemModel } from 'src/modules/transaction/refund/data/models/refund-item.model'; +import { TransactionTaxEntity } from '../../domain/entities/transaction-tax.entity'; @Entity(TABLE_NAME.TRANSACTION_ITEM) export class TransactionItemModel @@ -51,6 +52,12 @@ export class TransactionItemModel @Column('decimal', { name: 'item_tenant_share_margin', nullable: true }) item_tenant_share_margin: number; + @Column('decimal', { nullable: true }) + total_net_price: number; + + @Column('decimal', { nullable: true }) + discount_value: number; + // calculation data @Column('decimal', { name: 'total_price', nullable: true }) total_price: number; @@ -64,6 +71,15 @@ export class TransactionItemModel @Column('decimal', { name: 'total_share_tenant', nullable: true }) total_share_tenant: number; + @Column('decimal', { name: 'total_profit_share', nullable: true }) + total_profit_share: number; + + @Column('decimal', { nullable: true }) + payment_total_dpp: number; + + @Column('decimal', { nullable: true }) + payment_total_tax: number; + @Column('int', { name: 'qty', nullable: true }) qty: number; @@ -103,6 +119,13 @@ export class TransactionItemModel }, ) bundling_items: TransactionItemBreakdownModel[]; + + @OneToMany(() => TransactionItemTaxModel, (model) => model.transaction, { + cascade: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + item_taxes: TransactionItemTaxModel[]; } @Entity(TABLE_NAME.TRANSACTION_ITEM_BREAKDOWN) @@ -122,10 +145,93 @@ export class TransactionItemBreakdownModel extends BaseCoreModel TransactionItemModel, (model) => model.bundling_items, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) @JoinColumn({ name: 'transaction_item_id' }) transaction_item: TransactionItemModel; + + @OneToMany(() => TransactionBreakdownTaxModel, (model) => model.transaction, { + cascade: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + item_taxes: TransactionBreakdownTaxModel[]; +} + +@Entity(TABLE_NAME.TRANSACTION_ITEM_TAX) +export class TransactionItemTaxModel + extends BaseCoreModel + implements TransactionTaxEntity +{ + @Column('varchar', { name: 'tax_id', nullable: true }) + tax_id: string; + + @Column('varchar', { name: 'tax_name', nullable: true }) + tax_name: string; + + @Column('decimal', { name: 'taxt_value', nullable: true }) + taxt_value: number; + + @Column('decimal', { name: 'tax_total_value', nullable: true }) + tax_total_value: number; + + @Column('varchar', { name: 'transaction_id', nullable: true }) + transaction_id: string; + @ManyToOne(() => TransactionItemModel, (model) => model.taxes, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'transaction_id' }) + transaction: TransactionItemModel; +} + +@Entity(TABLE_NAME.TRANSACTION_ITEM_BREAKDOWN_TAX) +export class TransactionBreakdownTaxModel + extends BaseCoreModel + implements TransactionTaxEntity +{ + @Column('varchar', { name: 'tax_id', nullable: true }) + tax_id: string; + + @Column('varchar', { name: 'tax_name', nullable: true }) + tax_name: string; + + @Column('decimal', { name: 'taxt_value', nullable: true }) + taxt_value: number; + + @Column('decimal', { name: 'tax_total_value', nullable: true }) + tax_total_value: number; + + @Column('varchar', { name: 'transaction_id', nullable: true }) + transaction_id: string; + @ManyToOne(() => TransactionItemBreakdownModel, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'transaction_id' }) + transaction: TransactionItemBreakdownModel; } diff --git a/src/modules/transaction/transaction/data/models/transaction.model.ts b/src/modules/transaction/transaction/data/models/transaction.model.ts index 44d994e..97d312a 100644 --- a/src/modules/transaction/transaction/data/models/transaction.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction.model.ts @@ -36,6 +36,9 @@ export class TransactionModel @Column('int', { name: 'creator_counter_no', nullable: true }) creator_counter_no: number; + @Column('varchar', { name: 'creator_counter_name', nullable: true }) + creator_counter_name: string; + // season data @Column('varchar', { name: 'season_period_id', nullable: true }) season_period_id: string; @@ -148,6 +151,9 @@ export class TransactionModel @Column('date', { name: 'payment_date', nullable: true }) payment_date: Date; + @Column('date', { name: 'payment_date_bank', nullable: true }) + payment_date_bank: Date; + // calculation data @Column('decimal', { name: 'payment_sub_total', nullable: true }) payment_sub_total: number; diff --git a/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts b/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts index 9d3a575..e759c91 100644 --- a/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts +++ b/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts @@ -19,6 +19,8 @@ export interface TransactionItemEntity extends BaseCoreEntity { // calculation data total_price: number; + total_net_price: number; + discount_value: number; total_hpp: number; total_profit: number; total_share_tenant: number; @@ -32,6 +34,9 @@ export interface TransactionBundlingItemEntity extends BaseCoreEntity { item_id: string; item_name: string; hpp: number; + total_net_price: number; + discount_value: number; base_price: number; item_rates: number; + total_price?: number; } diff --git a/src/modules/transaction/transaction/domain/entities/transaction.entity.ts b/src/modules/transaction/transaction/domain/entities/transaction.entity.ts index d9d507b..8aa0b55 100644 --- a/src/modules/transaction/transaction/domain/entities/transaction.entity.ts +++ b/src/modules/transaction/transaction/domain/entities/transaction.entity.ts @@ -6,6 +6,7 @@ import { } from '../../constants'; import { STATUS } from 'src/core/strings/constants/base.constants'; import { TransactionItemEntity } from './transaction-item.entity'; +import { TransactionTaxEntity } from './transaction-tax.entity'; export interface TransactionEntity extends BaseStatusEntity { // general info @@ -13,6 +14,7 @@ export interface TransactionEntity extends BaseStatusEntity { type: TransactionType; invoice_code: string; creator_counter_no: number; // nomor pos transaksi dibuat + creator_counter_name: string; // name pos transaksi dibuat // season data season_period_id: string; @@ -54,6 +56,7 @@ export interface TransactionEntity extends BaseStatusEntity { payment_midtrans_token: string; payment_midtrans_url: string; payment_date: Date; + payment_date_bank: Date; // calculation data payment_sub_total: number; // total invoice tanpa discount @@ -87,4 +90,5 @@ export interface TransactionEntity extends BaseStatusEntity { calendar_link?: string; items: TransactionItemEntity[]; + taxes?: TransactionTaxEntity[]; } diff --git a/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts new file mode 100644 index 0000000..ea3557d --- /dev/null +++ b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts @@ -0,0 +1,267 @@ +import { Injectable } from '@nestjs/common'; +import { SalesPriceFormulaDataService } from 'src/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service'; +import { TaxDataService } from 'src/modules/transaction/tax/data/services/tax-data.service'; +import { TransactionEntity } from '../../entities/transaction.entity'; +import { calculateFormula } from 'src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper'; +import * as math from 'mathjs'; +import { + TransactionBundlingItemEntity, + TransactionItemEntity, +} from '../../entities/transaction-item.entity'; + +@Injectable() +export class PriceCalculator { + constructor( + private formulaService: SalesPriceFormulaDataService, + private taxService: TaxDataService, + ) {} + + private initialValue(formulas: any[]) { + const tax = {}; + for (const formula of formulas) { + tax[`${formula.name}_value`] = null; + } + + return tax; + } + + async calculate(transaction: TransactionEntity) { + const prices = []; + const transaction_taxes = []; + + for (let i = 0; i < transaction.items.length; i++) { + const item = transaction.items[i]; + const price = await this.calculateItem(item); + const priceValues = this.calculatePrice(price); + prices.push(priceValues); + transaction_taxes.push(price.taxesValue); + + if (item.bundling_items) { + for (let b = 0; b < item.bundling_items.length; b++) { + const bundling = item.bundling_items[b]; + + const bundlingPrice = await this.calculateItem(bundling); + const bundlingValues = this.calculatePrice(bundlingPrice); + + const item_taxes = { item_taxes: bundlingPrice.taxesValue }; + Object.assign(bundling, bundlingValues, item_taxes); + item.bundling_items[b] = bundling; + } + } + + const item_taxes = { item_taxes: price.taxesValue }; + Object.assign(item, priceValues, item_taxes); + transaction.items[i] = item; + } + + const tatal_taxes = this.mergeAndSumArrays(transaction_taxes); + transaction.taxes = tatal_taxes; + const { payment_total_dpp, ...otherValue } = + this.sumArrayBasedOnObjectKey(prices); + + return { + dpp_value: payment_total_dpp, + other: otherValue, + tax_datas: tatal_taxes, + }; + } + + async calculateItem( + transaction: TransactionItemEntity | TransactionBundlingItemEntity, + ) { + // const itemShare = 20 / 100; + // const profitShare = 10 / 100; + + const { profitShare, tenantShare } = await this.formulaService.itemTax( + transaction.item_id, + ); + + const tax = await this.taxService.taxKeyValue(); + /* Update constant from + - profit_share -> tenant_share + - item_share -> profit_share + */ + const taxes = { + ...tax, + tenant_share: tenantShare, + profit_share: profitShare, + }; + const dpp = await this.formulaService.salesPriceFormula(); + const taxFormula = await this.taxService.getManyByOptions({}); + const shareFormulas = await this.formulaService.profitShareFormula(); + const taxShareFormulas = shareFormulas.map((formula) => { + return { + ...formula, + name: formula.value_for, + }; + }); + const formulas = [...taxFormula, ...taxShareFormulas]; + const values = { + total: transaction.total_net_price, + ...this.initialValue(formulas), + ...taxes, + }; + + // const dpp = formulas.find((formula) => formula.value_for == 'dpp'); + + const dppValue = calculateFormula(dpp.formula_string, taxes, values.total); + + values['dpp'] = dppValue; + values['dpp_value'] = dppValue; + + let calledVariable = []; + const taxesValue = []; + do { + const valueFor = this.withNullValue(values, calledVariable); + + const formula = formulas.find((formula) => { + const name = formula['name'] ?? formula['value_for']; + return `${name}_value` == valueFor; + }); + let result = null; + + try { + result = math.evaluate(formula.formula_string, values); + calledVariable = []; + } catch (error) { + calledVariable.push(valueFor); + console.log(error); + } + + values[valueFor] = result; + if (result != null) { + const tax = taxFormula.find(({ id }) => id == formula.id); + if (tax != null) { + taxesValue.push({ + tax_id: tax?.id, + tax_name: tax?.name, + taxt_value: result, + tax_total_value: result, + transaction_id: transaction.id, + }); + } + } + // values[`${valueFor}_value`] = result; + } while (this.containsNullValue(values)); + + // const itemShareFormula = shareFormulas.find( + // (f) => f.value_for == 'tenant_share', + // ); + // values['tenant_share_value'] = math.evaluate( + // itemShareFormula.formula_string, + // values, + // ); + + // const profitShareFormula = shareFormulas.find( + // (f) => f.value_for == 'profit_share', + // ); + // values['profit_share_value'] = math.evaluate( + // profitShareFormula.formula_string, + // values, + // ); + + return { dpp_value: dppValue, tax_datas: values, taxesValue }; + } + + calculatePrice(prices: any) { + const data = prices.tax_datas; + const filteredObject: any = Object.keys(data) + .filter((key) => key.endsWith('_value')) + .reduce((acc, key) => ({ ...acc, [key]: data[key] }), {}); + + const { dpp_value, tenant_share_value, profit_share_value, ...tax } = + filteredObject; + const taxes = this.sumPriceObject(tax); + return { + total_profit_share: profit_share_value, + total_share_tenant: tenant_share_value, + payment_total_tax: taxes, + payment_total_dpp: dpp_value, + tax, + }; + } + + sumArrayBasedOnObjectKey(arr) { + return arr.reduce((acc, cur) => { + Object.keys(cur).forEach((key) => { + if (!acc[key]) { + acc[key] = 0; + } + acc[key] += cur[key]; + }); + return acc; + }, {}); + } + + sumPriceObject(taxes: any): number { + let total = 0; + for (const tax in taxes) { + total += taxes[tax] ?? 0; + } + + return total; + } + + containsNullValue(obj) { + if (typeof obj !== 'object' || obj === null) { + return obj === null; // Return true if the value itself is null + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (this.containsNullValue(obj[key])) { + return true; // If any nested value is null, return true + } + } + } + + return false; // If no null values are found, return false + } + + mergeAndSumArrays(arrays): any { + const mergedData = {}; + + arrays.forEach((arr) => { + arr.forEach((item) => { + if (!mergedData[item.tax_id]) { + mergedData[item.tax_id] = { + tax_name: item.tax_name, + taxt_value: 0, + tax_total_value: 0, + transaction_id: item.transaction_id, + }; + } + + mergedData[item.tax_id].taxt_value += item.taxt_value; + mergedData[item.tax_id].tax_total_value += item.tax_total_value; + }); + }); + + return Object.values(mergedData); + } + + withNullValue(mainObj, called = []) { + const obj = { ...mainObj }; + for (const variable of called) { + delete obj[variable]; + } + if (typeof obj !== 'object' || obj === null) { + return null; // Return null for non-object values + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (obj[key] === null) { + return key; // Found a null value, return the key + } else if (typeof obj[key] === 'object') { + const nestedKey = this.withNullValue(obj[key]); + if (nestedKey !== null) { + return `${key}.${nestedKey}`; // Found null in nested object, return nested key path + } + } + } + } + + return null; // No null values found, return null + } +} diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts index 1df1f2b..fcddbd3 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts @@ -61,11 +61,12 @@ export class PosTransactionHandler implements IEventHandler { // jika delete if (data._deleted ?? false) { - await this.dataService.deleteById( - queryRunner, - TransactionModel, - data._id, - ); + // FIXME => This comment for ignore delete from POS data + // await this.dataService.deleteById( + // queryRunner, + // TransactionModel, + // data._id, + // ); } // jika update // create diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/settled-transaction.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/settled-transaction.handler.ts index 0bf4e4d..9217f1b 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/settled-transaction.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/settled-transaction.handler.ts @@ -9,6 +9,7 @@ import { TransactionModel } from '../../../data/models/transaction.model'; import { calculateSalesFormula } from 'src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper'; import { CreateEventCalendarHelper } from 'src/modules/configuration/google-calendar/domain/usecases/managers/helpers/create-event-calanedar.helper'; import { TransactionUpdatedEvent } from '../../entities/event/transaction-updated.event'; +import { PriceCalculator } from '../calculator/price.calculator'; @EventsHandler(TransactionChangeStatusEvent, TransactionUpdatedEvent) export class SettledTransactionHandler @@ -18,6 +19,7 @@ export class SettledTransactionHandler private formulaService: SalesPriceFormulaDataService, private taxService: TaxDataService, private dataService: TransactionDataService, + private calculator: PriceCalculator, ) {} async handle(event: TransactionChangeStatusEvent) { @@ -62,12 +64,16 @@ export class SettledTransactionHandler }); // const profit_share_value = this.calculateFormula(profit_formula.formula_string, taxes, data.payment_total_net_profit ?? 0); - const { dpp_value, tax_datas } = calculateSalesFormula( - sales_price.formula_string, - taxes, - data.payment_total_net_profit ?? 0, + const { dpp_value, tax_datas, other } = await this.calculator.calculate( + data, ); + // calculateSalesFormula( + // sales_price.formula_string, + // taxes, + // data.payment_total_net_profit ?? 0, + // ); + // console.log(data, 'dsa'); const google_calendar = await CreateEventCalendarHelper(data); @@ -78,6 +84,8 @@ export class SettledTransactionHandler taxes: tax_datas, calendar_id: google_calendar?.id, calendar_link: google_calendar?.htmlLink, + payment_total_share: other.total_profit_share, + payment_total_tax: other.payment_total_tax, }); } else if (oldSettled) { // console.log(data, 'data oldSettled'); diff --git a/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts b/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts index d577c4d..b788110 100644 --- a/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts +++ b/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts @@ -42,6 +42,7 @@ export class DetailTransactionManager extends BaseDetailManager 0 + ? +discount_value / +payment_sub_total + : discount_percentage ?? 0 / 100; + const discountValue = payment_sub_total * discountPercent; + Object.assign(data, { payment_total_net_profit: data.payment_total, + discount_value: discountValue, + discount_percentage: discountPercent * 100, customer_category_id: data.customer_category?.id ?? null, customer_category_name: data.customer_category?.name ?? null, season_period_id: data.season_period?.id ?? null, @@ -173,10 +183,31 @@ export function mappingRevertTransaction(data, type) { data.items?.map((item) => { const total_price = Number(item.item.price ?? item.item.base_price) * Number(item.qty); + const discount = discountPercent * total_price; + const net_price = total_price - discount; const share_margin = item.item.tenant?.share_margin ?? 0; const total_share_tenant = share_margin > 0 ? (Number(share_margin) / 100) * total_price : 0; + item.bundling_items = item.item.bundling_items?.map((bundling) => { + if (bundling.item_id) return bundling; + + const basePrice = + (bundling.item_rates ?? bundling.base_price) * +item.qty; + const discount = discountPercent * basePrice; + const total = basePrice - discount; + + return { + ...bundling, + item_id: bundling.id, + item_name: bundling.name, + total_net_price: basePrice, + discount_value: discount, + total_price: total, + id: uuidv4(), + }; + }); + Object.assign(item, { item_id: item.item.id, item_name: item.item.name, @@ -191,20 +222,13 @@ export function mappingRevertTransaction(data, type) { (bundling) => bundling.name, ), breakdown_bundling: item.item.breakdown_bundling, - bundling_items: item.item.bundling_items?.map((bundling) => { - if (bundling.item_id) return bundling; - return { - ...bundling, - item_id: bundling.id, - item_name: bundling.name, - id: uuidv4(), - }; - }), item_tenant_id: item.item.tenant?.id ?? null, item_tenant_name: item.item.tenant?.id ?? null, item_tenant_share_margin: item.item.tenant?.share_margin ?? null, + total_net_price: net_price, + discount_value: discount, total_price: total_price, total_hpp: Number(item.item.hpp) * Number(item.qty), total_share_tenant: total_share_tenant, diff --git a/src/modules/transaction/transaction/domain/usecases/managers/index-transaction.manager.ts b/src/modules/transaction/transaction/domain/usecases/managers/index-transaction.manager.ts index 122920e..7e31403 100644 --- a/src/modules/transaction/transaction/domain/usecases/managers/index-transaction.manager.ts +++ b/src/modules/transaction/transaction/domain/usecases/managers/index-transaction.manager.ts @@ -55,6 +55,7 @@ export class IndexTransactionManager extends BaseIndexManager `${this.tableName}.status`, `${this.tableName}.invoice_code`, `${this.tableName}.creator_counter_no`, + `${this.tableName}.creator_counter_name`, `${this.tableName}.booking_date`, `${this.tableName}.no_of_group`, `${this.tableName}.type`, diff --git a/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts b/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts index 8496379..c717131 100644 --- a/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts +++ b/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts @@ -99,6 +99,7 @@ export class TransactionDataOrchestrator { return this.batchCancelManager.getResult(); } + // Confirm from draft to pending async confirm(dataId): Promise { this.confirmManager.setData(dataId, STATUS.ACTIVE); this.confirmManager.setService( @@ -111,7 +112,7 @@ export class TransactionDataOrchestrator { } async batchConfirm(dataIds: string[]): Promise { - this.batchConfirmManager.setData(dataIds, STATUS.ACTIVE); + this.batchConfirmManager.setData(dataIds, STATUS.PENDING); this.batchConfirmManager.setService( this.serviceData, TABLE_NAME.TRANSACTION, diff --git a/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts b/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts index 4f39239..cbc1912 100644 --- a/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts +++ b/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts @@ -6,6 +6,7 @@ import { PaginationResponse } from 'src/core/response/domain/ok-response.interfa import { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-read.orchestrator'; import { DetailTransactionManager } from './managers/detail-transaction.manager'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { PriceCalculator } from './calculator/price.calculator'; @Injectable() export class TransactionReadOrchestrator extends BaseReadOrchestrator { @@ -13,6 +14,7 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator { + const transaction = await this.serviceData.getOneByOptions({ + where: { + id, + }, + relations: ['items', 'items.bundling_items'], + }); + + const price = await this.calculator.calculate(transaction); + transaction.payment_total_profit = + transaction.payment_total - + price.other.total_profit_share - + price.other.payment_total_tax - + price.other.total_share_tenant; + transaction.payment_total_dpp = price.dpp_value; + transaction.payment_total_share = price.other.total_profit_share; + transaction.payment_total_tax = price.other.payment_total_tax; + console.log({ price }, transaction.payment_total); + } + + async calculatePrice(): Promise { + const transactions = await this.serviceData.getManyByOptions({ + where: { + is_recap_transaction: false, + }, + relations: ['items', 'items.bundling_items'], + }); + + for (const transaction of transactions) { + try { + const price = await this.calculator.calculate(transaction); + transaction.payment_total_profit = + transaction.payment_total - + price.other.total_profit_share - + price.other.payment_total_tax - + price.other.total_share_tenant; + transaction.payment_total_dpp = price.dpp_value; + transaction.payment_total_share = price.other.total_profit_share; + transaction.payment_total_tax = price.other.payment_total_tax; + console.log(transaction.id); + await this.serviceData.getRepository().save(transaction); + + // break; + } catch (error) { + console.log(error); + } + } + } } diff --git a/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts index 099cc0d..8446857 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts @@ -27,4 +27,18 @@ export class TransactionReadController { async detail(@Param('id') id: string): Promise { return await this.orchestrator.detail(id); } + + @Public(true) + @Get('dummy/:id') + async calculate(@Param('id') id: string): Promise { + this.orchestrator.dummyCalculate(id); + return 'OK'; + } + + @Public(true) + @Get('dummy2/calculate') + async calculateAll(): Promise { + this.orchestrator.calculatePrice(); + return 'OK'; + } } diff --git a/src/modules/transaction/transaction/transaction.module.ts b/src/modules/transaction/transaction/transaction.module.ts index f4a2e61..e08f2db 100644 --- a/src/modules/transaction/transaction/transaction.module.ts +++ b/src/modules/transaction/transaction/transaction.module.ts @@ -19,8 +19,10 @@ import { BatchDeleteTransactionManager } from './domain/usecases/managers/batch- import { BatchConfirmTransactionManager } from './domain/usecases/managers/batch-confirm-transaction.manager'; import { TransactionModel } from './data/models/transaction.model'; import { + TransactionBreakdownTaxModel, TransactionItemBreakdownModel, TransactionItemModel, + TransactionItemTaxModel, } from './data/models/transaction-item.model'; import { TransactionTaxModel } from './data/models/transaction-tax.model'; import { CancelTransactionManager } from './domain/usecases/managers/cancel-transaction.manager'; @@ -39,6 +41,8 @@ import { PdfMakeManager } from 'src/modules/configuration/export/domain/managers import { PaymentMethodDataService } from '../payment-method/data/services/payment-method-data.service'; import { PaymentMethodModel } from '../payment-method/data/models/payment-method.model'; import { TransactionDemographyModel } from './data/models/transaction-demography.model'; +import { PriceCalculator } from './domain/usecases/calculator/price.calculator'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; @Module({ exports: [TransactionReadService], @@ -51,9 +55,12 @@ import { TransactionDemographyModel } from './data/models/transaction-demography TransactionDemographyModel, TransactionItemBreakdownModel, TransactionTaxModel, + TransactionItemTaxModel, + TransactionBreakdownTaxModel, TaxModel, SalesPriceFormulaModel, PaymentMethodModel, + ItemModel, ], CONNECTION_NAME.DEFAULT, ), @@ -61,6 +68,7 @@ import { TransactionDemographyModel } from './data/models/transaction-demography ], controllers: [TransactionDataController, TransactionReadController], providers: [ + PriceCalculator, RefundUpdatedHandler, PosTransactionHandler, MidtransCallbackHandler, diff --git a/src/modules/transaction/vip-code/vip-code.module.ts b/src/modules/transaction/vip-code/vip-code.module.ts index e3b98ca..bc756d5 100644 --- a/src/modules/transaction/vip-code/vip-code.module.ts +++ b/src/modules/transaction/vip-code/vip-code.module.ts @@ -14,13 +14,14 @@ import { IndexVipCodeManager } from './domain/usecases/managers/index-vip-code.m import { VipCodeModel } from './data/models/vip-code.model'; import { GenerateVipCodeManager } from './domain/usecases/managers/geneate-vip-code.manager'; import { CreateVipCodeHandler } from './domain/usecases/handlers/create-vip-code.handler'; -import { CouchService } from 'src/modules/configuration/couch/data/services/couch.service'; +import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature([VipCodeModel], CONNECTION_NAME.DEFAULT), CqrsModule, + CouchModule, ], controllers: [VipCodeDataController, VipCodeReadController], providers: [ @@ -30,7 +31,6 @@ import { CouchService } from 'src/modules/configuration/couch/data/services/couc CreateVipCodeManager, GenerateVipCodeManager, - CouchService, VipCodeDataService, VipCodeReadService, diff --git a/src/modules/user-related/tenant/infrastructure/dto/tenant.dto.ts b/src/modules/user-related/tenant/infrastructure/dto/tenant.dto.ts index 48076e4..9d2328b 100644 --- a/src/modules/user-related/tenant/infrastructure/dto/tenant.dto.ts +++ b/src/modules/user-related/tenant/infrastructure/dto/tenant.dto.ts @@ -35,7 +35,4 @@ export class TenantDto extends BaseStatusDto implements UserEntity { @Exclude() role: UserRole; - - @Exclude() - refresh_token: string; } diff --git a/src/modules/user-related/tenant/infrastructure/dto/update-password-tenant.dto.ts b/src/modules/user-related/tenant/infrastructure/dto/update-password-tenant.dto.ts index a4fee8b..ed85930 100644 --- a/src/modules/user-related/tenant/infrastructure/dto/update-password-tenant.dto.ts +++ b/src/modules/user-related/tenant/infrastructure/dto/update-password-tenant.dto.ts @@ -27,7 +27,4 @@ export class UpdatePasswordTenantDto @Exclude() role: UserRole; - - @Exclude() - refresh_token: string; } diff --git a/src/modules/user-related/tenant/infrastructure/dto/update-tenant.dto.ts b/src/modules/user-related/tenant/infrastructure/dto/update-tenant.dto.ts index 14487dc..666aaff 100644 --- a/src/modules/user-related/tenant/infrastructure/dto/update-tenant.dto.ts +++ b/src/modules/user-related/tenant/infrastructure/dto/update-tenant.dto.ts @@ -34,7 +34,4 @@ export class UpdateTenantDto extends BaseStatusDto implements UserEntity { @Exclude() role: UserRole; - - @Exclude() - refresh_token: string; } diff --git a/src/modules/user-related/tenant/tenant.module.ts b/src/modules/user-related/tenant/tenant.module.ts index fe10160..24f54b4 100644 --- a/src/modules/user-related/tenant/tenant.module.ts +++ b/src/modules/user-related/tenant/tenant.module.ts @@ -25,11 +25,15 @@ import { UserDataService } from '../user/data/services/user-data.service'; import { UserReadService } from '../user/data/services/user-read.service'; import { TenantItemReadController } from './infrastructure/tenant-item-read.controller'; import { TenantItemDataController } from './infrastructure/tenant-item-data.controller'; +import { UserLoginModel } from '../user/data/models/user-login.model'; @Module({ imports: [ ConfigModule.forRoot(), - TypeOrmModule.forFeature([UserModel], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [UserModel, UserLoginModel], + CONNECTION_NAME.DEFAULT, + ), CqrsModule, ], controllers: [ diff --git a/src/modules/user-related/user/constants.ts b/src/modules/user-related/user/constants.ts index 251f468..ea5b4a4 100644 --- a/src/modules/user-related/user/constants.ts +++ b/src/modules/user-related/user/constants.ts @@ -2,4 +2,5 @@ export enum UserRole { SUPERADMIN = 'superadmin', STAFF = 'staff', TENANT = 'tenant', + QUEUE_ADMIN = 'queue_admin', } diff --git a/src/modules/user-related/user/data/models/user-login.model.ts b/src/modules/user-related/user/data/models/user-login.model.ts new file mode 100644 index 0000000..baf4442 --- /dev/null +++ b/src/modules/user-related/user/data/models/user-login.model.ts @@ -0,0 +1,44 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { UserEntity } from '../../domain/entities/user.entity'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; +import { UserLoginEntity } from '../../domain/entities/user-login.entity'; +import { UserModel } from './user.model'; +import { BaseCoreModel } from 'src/core/modules/data/model/base-core.model'; +import { UserRole } from '../../constants'; +import { AppSource } from 'src/core/helpers/constant'; + +@Entity(TABLE_NAME.USER_LOGIN) +export class UserLoginModel + extends BaseCoreModel + implements UserLoginEntity +{ + @Column({ type: 'bigint', nullable: false }) + login_date: number; + + @Column({ type: 'varchar', name: 'login_token', nullable: true }) + login_token: string; + + @Column('varchar', { name: 'user_id', nullable: true }) + user_id: string; + + @Column({ type: 'uuid', nullable: true }) + item_id: string; + + @Column({ type: 'varchar', nullable: true }) + item_name: string; + + @Column({ type: 'enum', enum: UserRole, nullable: true }) + role: UserRole; + + @Column({ type: 'enum', enum: AppSource, nullable: true }) + source: AppSource; + + @ManyToOne(() => UserModel, (model) => model.user_login, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + nullable: false, + orphanedRowAction: 'delete', + }) + @JoinColumn({ name: 'user_id' }) + user: UserModel; +} diff --git a/src/modules/user-related/user/data/models/user.model.ts b/src/modules/user-related/user/data/models/user.model.ts index 55eb6a9..ff0a6f8 100644 --- a/src/modules/user-related/user/data/models/user.model.ts +++ b/src/modules/user-related/user/data/models/user.model.ts @@ -1,19 +1,24 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { UserEntity } from '../../domain/entities/user.entity'; -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, +} from 'typeorm'; import { BaseStatusModel } from 'src/core/modules/data/model/base-status.model'; import { UserPrivilegeModel } from 'src/modules/user-related/user-privilege/data/models/user-privilege.model'; import { UserRole } from '../../constants'; import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; +import { UserLoginModel } from './user-login.model'; @Entity(TABLE_NAME.USER) export class UserModel extends BaseStatusModel implements UserEntity { - @Column('varchar', { name: 'refresh_token', nullable: true }) - refresh_token: string; - @Column('varchar', { name: 'name', nullable: true }) name: string; @@ -48,4 +53,10 @@ export class UserModel onUpdate: 'CASCADE', }) items: ItemModel[]; + + // relasi ke user login for admin queue + @OneToMany(() => UserLoginModel, (model) => model.user, { + cascade: true, + }) + user_login: UserLoginModel; } diff --git a/src/modules/user-related/user/data/services/user-data.service.ts b/src/modules/user-related/user/data/services/user-data.service.ts index 4f69f7c..9a5a781 100644 --- a/src/modules/user-related/user/data/services/user-data.service.ts +++ b/src/modules/user-related/user/data/services/user-data.service.ts @@ -3,15 +3,85 @@ import { BaseDataService } from 'src/core/modules/data/service/base-data.service import { UserEntity } from '../../domain/entities/user.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { UserModel } from '../models/user.model'; -import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; -import { Repository } from 'typeorm'; +import { + CONNECTION_NAME, + OPERATION, +} from 'src/core/strings/constants/base.constants'; +import { IsNull, Not, Repository } from 'typeorm'; +import { UserLoginModel } from '../models/user-login.model'; +import { UserLoginEntity } from '../../domain/entities/user-login.entity'; +import { LogUserType } from 'src/core/helpers/constant'; +import { EventBus } from '@nestjs/cqrs'; +import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event'; @Injectable() export class UserDataService extends BaseDataService { constructor( @InjectRepository(UserModel, CONNECTION_NAME.DEFAULT) private repo: Repository, + + @InjectRepository(UserLoginModel, CONNECTION_NAME.DEFAULT) + private repoLoginUser: Repository, + + private eventBus: EventBus, ) { super(repo); } + + async getLoginUserByItem(itemId: string) { + return this.repoLoginUser.findOne({ + where: { item_id: itemId, user_id: Not(IsNull()) }, + }); + } + + async saveUserLogin(userLogin: UserLoginEntity) { + return this.repoLoginUser.save(userLogin); + } + + async removeUserLogin(userLogin: Partial) { + return this.repoLoginUser.delete(userLogin); + } + + async forceLogout(token: string) { + const data = await this.repoLoginUser.findOneBy({ login_token: token }); + + if (data) return; + else { + await this.repoLoginUser.delete({ login_token: token }); + + const [, body] = token.split('.'); + const bodyToken = JSON.parse(atob(body)); + const user = { + role: bodyToken.role, + user_id: bodyToken.id, + username: bodyToken.username, + user_privilege_id: bodyToken.user_privilege_id, + item_id: bodyToken.item_id, + item_name: bodyToken.item_name, + source: bodyToken.source, + }; + + const userLogout = { + type: LogUserType.logout, + created_at: new Date().getTime(), + name: user.username, + user_privilege_id: user.user_privilege_id, + ...user, + }; + + this.eventBus.publish( + new LogUserLoginEvent({ + id: user.user_id, + old: null, + data: userLogout, + user: userLogout as any, + description: 'Logout', + module: UserModel.name, + op: OPERATION.UPDATE, + }), + ); + + return; + } + } } diff --git a/src/modules/user-related/user/domain/entities/user-login.entity.ts b/src/modules/user-related/user/domain/entities/user-login.entity.ts new file mode 100644 index 0000000..cc940e2 --- /dev/null +++ b/src/modules/user-related/user/domain/entities/user-login.entity.ts @@ -0,0 +1,13 @@ +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; +import { UserRole } from '../../constants'; +import { AppSource } from 'src/core/helpers/constant'; + +export interface UserLoginEntity extends BaseCoreEntity { + login_date: number; + login_token: string; + user_id: string; + item_id: string; + item_name: string; + role: UserRole; + source: AppSource; +} diff --git a/src/modules/user-related/user/domain/entities/user.entity.ts b/src/modules/user-related/user/domain/entities/user.entity.ts index 11f3363..fcaaae6 100644 --- a/src/modules/user-related/user/domain/entities/user.entity.ts +++ b/src/modules/user-related/user/domain/entities/user.entity.ts @@ -6,7 +6,6 @@ export interface UserEntity extends BaseStatusEntity { username: string; password: string; role: UserRole; - refresh_token: string; // tenant data share_margin: number; diff --git a/src/modules/user-related/user/domain/usecases/managers/create-user.manager.ts b/src/modules/user-related/user/domain/usecases/managers/create-user.manager.ts index ba2c477..f57e40d 100644 --- a/src/modules/user-related/user/domain/usecases/managers/create-user.manager.ts +++ b/src/modules/user-related/user/domain/usecases/managers/create-user.manager.ts @@ -15,12 +15,7 @@ import { SALT_OR_ROUNDS } from 'src/core/strings/constants/base.constants'; @Injectable() export class CreateUserManager extends BaseCreateManager { async beforeProcess(): Promise { - let role = UserRole.STAFF; - if (this.data.is_super_admin || !this.data.user_privilege) - role = UserRole.SUPERADMIN; - Object.assign(this.data, { - role: role, password: await hashPassword(this.data.password, SALT_OR_ROUNDS), }); diff --git a/src/modules/user-related/user/domain/usecases/managers/detail-user.manager.ts b/src/modules/user-related/user/domain/usecases/managers/detail-user.manager.ts index 7e28045..7a69f00 100644 --- a/src/modules/user-related/user/domain/usecases/managers/detail-user.manager.ts +++ b/src/modules/user-related/user/domain/usecases/managers/detail-user.manager.ts @@ -36,6 +36,7 @@ export class DetailUserManager extends BaseDetailManager { `${this.tableName}.status`, `${this.tableName}.name`, `${this.tableName}.username`, + `${this.tableName}.role`, `${this.tableName}.created_at`, `${this.tableName}.creator_name`, `${this.tableName}.updated_at`, diff --git a/src/modules/user-related/user/domain/usecases/managers/index-user.manager.ts b/src/modules/user-related/user/domain/usecases/managers/index-user.manager.ts index 1d6869c..c437dc7 100644 --- a/src/modules/user-related/user/domain/usecases/managers/index-user.manager.ts +++ b/src/modules/user-related/user/domain/usecases/managers/index-user.manager.ts @@ -28,7 +28,7 @@ export class IndexUserManager extends BaseIndexManager { joinRelations: [], // relation join and select (relasi yang ingin ditampilkan), - selectRelations: ['user_privilege'], + selectRelations: ['user_privilege', 'user_login'], // relation yang hanya ingin dihitung (akan return number) countRelations: [], @@ -41,6 +41,7 @@ export class IndexUserManager extends BaseIndexManager { `${this.tableName}.status`, `${this.tableName}.name`, `${this.tableName}.username`, + `${this.tableName}.role`, `${this.tableName}.created_at`, `${this.tableName}.creator_name`, `${this.tableName}.updated_at`, @@ -48,6 +49,12 @@ export class IndexUserManager extends BaseIndexManager { 'user_privilege.id', 'user_privilege.name', + + 'user_login.id', + 'user_login.login_date', + 'user_login.item_id', + 'user_login.item_name', + 'user_login.source', ]; } diff --git a/src/modules/user-related/user/domain/usecases/managers/update-user.manager.ts b/src/modules/user-related/user/domain/usecases/managers/update-user.manager.ts index 360bd70..3644a63 100644 --- a/src/modules/user-related/user/domain/usecases/managers/update-user.manager.ts +++ b/src/modules/user-related/user/domain/usecases/managers/update-user.manager.ts @@ -8,7 +8,6 @@ import { columnUniques, validateRelations, } from 'src/core/strings/constants/interface.constants'; -import { UserRole } from '../../../constants'; @Injectable() export class UpdateUserManager extends BaseUpdateManager { @@ -17,14 +16,6 @@ export class UpdateUserManager extends BaseUpdateManager { } async beforeProcess(): Promise { - let role = UserRole.STAFF; - if (this.data.is_super_admin || !this.data.user_privilege) - role = UserRole.SUPERADMIN; - - Object.assign(this.data, { - role: role, - }); - return; } diff --git a/src/modules/user-related/user/domain/usecases/user-data.orchestrator.ts b/src/modules/user-related/user/domain/usecases/user-data.orchestrator.ts index b77e469..ea2b9ec 100644 --- a/src/modules/user-related/user/domain/usecases/user-data.orchestrator.ts +++ b/src/modules/user-related/user/domain/usecases/user-data.orchestrator.ts @@ -31,6 +31,7 @@ export class UserDataOrchestrator extends BaseDataTransactionOrchestrator body.user_privilege) user_privilege: UserPrivilegeModel; - @ApiProperty({ - name: 'is_super_admin', - type: Boolean, - required: true, - example: false, - }) - @IsBoolean() - is_super_admin: boolean; + @ApiProperty({ name: 'role', required: true, example: UserRole.STAFF }) + @IsString() + role: UserRole; @Exclude() share_margin: number; @Exclude() email: string; - - @Exclude() - role: UserRole; - - @Exclude() - refresh_token: string; } diff --git a/src/modules/user-related/user/user.module.ts b/src/modules/user-related/user/user.module.ts index bb4d2b5..4f92e9d 100644 --- a/src/modules/user-related/user/user.module.ts +++ b/src/modules/user-related/user/user.module.ts @@ -23,11 +23,15 @@ import { BatchConfirmUserManager } from './domain/usecases/managers/batch-confir import { BatchInactiveUserManager } from './domain/usecases/managers/batch-inactive-user.manager'; import { UserModel } from './data/models/user.model'; import { UpdatePasswordUserManager } from './domain/usecases/managers/update-password-user.manager'; +import { UserLoginModel } from './data/models/user-login.model'; @Module({ imports: [ ConfigModule.forRoot(), - TypeOrmModule.forFeature([UserModel], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [UserModel, UserLoginModel], + CONNECTION_NAME.DEFAULT, + ), CqrsModule, ], controllers: [UserDataController, UserReadController],