From 614a9346fbca097b59504ad5b45eda98a155fdcd Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 18 Sep 2024 13:55:47 +0700 Subject: [PATCH] feat: price calculator --- .../1726045820711-add-tax-item-transaction.ts | 36 +++- .../1726365023179-add-formula-to-tax.ts | 17 ++ .../sales-price-formula-data.service.ts | 22 +++ .../helpers/calculation-formula.helper.ts | 13 +- .../transaction/tax/data/models/tax.model.ts | 6 + .../tax/data/services/tax-data.service.ts | 2 +- .../tax/domain/entities/tax.entity.ts | 1 + .../data/models/transaction-item.model.ts | 15 ++ .../entities/transaction-item.entity.ts | 1 + .../usecases/calculator/price.calculator.ts | 166 +++++++++++++++--- .../usecases/transaction-read.orchestrator.ts | 51 ++++++ .../transaction-read.controller.ts | 14 ++ .../transaction/transaction.module.ts | 2 + 13 files changed, 306 insertions(+), 40 deletions(-) rename 1726045820711-add-tax-item-transaction.ts => src/database/migrations/1726045820711-add-tax-item-transaction.ts (68%) create mode 100644 src/database/migrations/1726365023179-add-formula-to-tax.ts diff --git a/1726045820711-add-tax-item-transaction.ts b/src/database/migrations/1726045820711-add-tax-item-transaction.ts similarity index 68% rename from 1726045820711-add-tax-item-transaction.ts rename to src/database/migrations/1726045820711-add-tax-item-transaction.ts index 93634fa..7dffa6a 100644 --- a/1726045820711-add-tax-item-transaction.ts +++ b/src/database/migrations/1726045820711-add-tax-item-transaction.ts @@ -16,15 +16,27 @@ export class AddTaxItemTransaction1726045820711 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "transaction_item_breakdowns" ADD "total_profit_share" numeric`, ); - await queryRunner.query( - `ALTER TABLE "price_formulas" ADD "value_for" character varying NOT NULL DEFAULT 'dpp'`, - ); 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 { @@ -34,9 +46,6 @@ export class AddTaxItemTransaction1726045820711 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "transaction_item_taxes" DROP CONSTRAINT "FK_f5c4966a381d903899cafb4b5ba"`, ); - await queryRunner.query( - `ALTER TABLE "price_formulas" DROP COLUMN "value_for"`, - ); await queryRunner.query( `ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "total_profit_share"`, ); @@ -45,5 +54,20 @@ export class AddTaxItemTransaction1726045820711 implements MigrationInterface { ); 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/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/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service.ts b/src/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service.ts index 5d71513..89a45ae 100644 --- a/src/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service.ts +++ b/src/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service.ts @@ -6,12 +6,15 @@ import { SalesPriceFormulaModel } from '../models/sales-price-formula.model'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { Repository } from 'typeorm'; import { FormulaType } from '../../constants'; +import { TaxModel } from 'src/modules/transaction/tax/data/models/tax.model'; @Injectable() export class SalesPriceFormulaDataService extends BaseDataService { constructor( @InjectRepository(SalesPriceFormulaModel, CONNECTION_NAME.DEFAULT) private repo: Repository, + @InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT) + private tax: Repository, ) { super(repo); } @@ -23,4 +26,23 @@ export class SalesPriceFormulaDataService extends BaseDataService 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 24c45f7..7b5d70c 100644 --- a/src/modules/transaction/tax/data/services/tax-data.service.ts +++ b/src/modules/transaction/tax/data/services/tax-data.service.ts @@ -29,7 +29,7 @@ export class TaxDataService extends BaseDataService { for (const tax of taxes) { const { name, value } = tax; - keyVal[name] = value; + 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/transaction/data/models/transaction-item.model.ts b/src/modules/transaction/transaction/data/models/transaction-item.model.ts index 7164925..01b5019 100644 --- a/src/modules/transaction/transaction/data/models/transaction-item.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction-item.model.ts @@ -68,6 +68,12 @@ export class TransactionItemModel @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; @@ -129,6 +135,15 @@ export class TransactionItemBreakdownModel extends BaseCoreModel TransactionItemModel, (model) => model.bundling_items, { onDelete: 'CASCADE', onUpdate: 'CASCADE', 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..ba31418 100644 --- a/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts +++ b/src/modules/transaction/transaction/domain/entities/transaction-item.entity.ts @@ -34,4 +34,5 @@ export interface TransactionBundlingItemEntity extends BaseCoreEntity { hpp: number; base_price: number; item_rates: number; + total_price?: number; } diff --git a/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts index 527a771..2c109d2 100644 --- a/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts +++ b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts @@ -3,6 +3,11 @@ import { SalesPriceFormulaDataService } from 'src/modules/transaction/sales-pric 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 { @@ -11,56 +16,159 @@ export class PriceCalculator { private taxService: TaxDataService, ) {} - private initialValue(tax: any) { - if (typeof tax !== 'object' || tax === null) { - return null; // Return null for non-object values + private initialValue(formulas: any[]) { + const tax = {}; + for (const formula of formulas) { + tax[`${formula.name}_value`] = null; } - for (const key in tax) { - if (tax.hasOwnProperty(key)) { - tax[key] = null; - } - } - - tax['dpp'] = null; - return tax; } async calculate(transaction: TransactionEntity) { - const taxes = await this.taxService.taxKeyValue(); - const formulas = await this.formulaService.profitShareFormula(); - const values = this.initialValue(taxes); + const prices = []; + // for (const item of transaction.items) { + // const price = await this.calculateItem(item); + // const priceValues = this.calculatePrice(price); + + // prices.push(priceValues); + // } + + 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); + + if (item.bundling_items) { + for (let b = 0; b < item.bundling_items.length; b++) { + const bundling = item.bundling_items[b]; + bundling.total_price = bundling.item_rates ?? bundling.base_price; + + const bundlingPrice = await this.calculateItem(bundling); + const bundlingValues = this.calculatePrice(bundlingPrice); + + Object.assign(bundling, bundlingValues); + item.bundling_items[b] = bundling; + } + } + + Object.assign(item, priceValues); + transaction.items[i] = item; + } + + const { payment_total_dpp, ...otherValue } = + this.sumArrayBasedOnObjectKey(prices); + + return { dpp_value: payment_total_dpp, other: otherValue, tax_datas: [] }; + } + + async calculateItem( + transaction: TransactionItemEntity | TransactionBundlingItemEntity, + ) { + const itemShare = 20 / 100; + const profitShare = 10 / 100; + + const tax = await this.taxService.taxKeyValue(); + const taxes = { ...tax, item_share: itemShare, profit_share: profitShare }; + const formulas = await this.formulaService.salesPriceFormula(); + const taxFormula = await this.taxService.getManyByOptions({}); + const shareFormulas = await this.formulaService.profitShareFormula(); + const values = { + total: transaction.total_price, + ...this.initialValue(taxFormula), + ...taxes, + }; + + // const dpp = formulas.find((formula) => formula.value_for == 'dpp'); + const dpp = formulas; - const dpp = formulas.find((formula) => formula.value_for == 'dpp'); const dppValue = calculateFormula( dpp.formula_string, taxes, - transaction.payment_total_net_profit, + transaction.total_price, ); values['dpp'] = dppValue; + values['dpp_value'] = dppValue; + let calledVariable = []; do { - const valueFor = this.withNullValue(values); - const formula = formulas[valueFor]; - let result; + const valueFor = this.withNullValue(values, calledVariable); + const formula = taxFormula.find( + (formula) => `${formula.name}_value` == valueFor, + ); + let result = null; try { - result = calculateFormula( - formula.formula_string, - values, - transaction.payment_total_net_profit, - ); - } catch (error) {} + result = math.evaluate(formula.formula_string, values); + calledVariable = []; + } catch (error) { + calledVariable.push(valueFor); + console.log(error); + } values[valueFor] = result; - values[`${valueFor}_value`] = result; + // values[`${valueFor}_value`] = result; } while (this.containsNullValue(values)); + const itemShareFormula = shareFormulas.find( + (f) => f.value_for == 'item_share', + ); + values['item_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 }; } + 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, item_share_value, profit_share_value, ...tax } = + filteredObject; + const taxes = this.sumPriceObject(tax); + return { + total_profit_share: item_share_value, + total_share_tenant: profit_share_value, + payment_total_tax: taxes, + payment_total_dpp: dpp_value, + }; + } + + 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 @@ -77,7 +185,11 @@ export class PriceCalculator { return false; // If no null values are found, return false } - withNullValue(obj) { + 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 } 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..82a2180 100644 --- a/src/modules/transaction/transaction/transaction.module.ts +++ b/src/modules/transaction/transaction/transaction.module.ts @@ -39,6 +39,7 @@ 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'; @Module({ exports: [TransactionReadService], @@ -61,6 +62,7 @@ import { TransactionDemographyModel } from './data/models/transaction-demography ], controllers: [TransactionDataController, TransactionReadController], providers: [ + PriceCalculator, RefundUpdatedHandler, PosTransactionHandler, MidtransCallbackHandler,