feat: price calculator

pull/93/head
shancheas 2024-09-18 13:55:47 +07:00
parent 19494b3328
commit 614a9346fb
13 changed files with 306 additions and 40 deletions

View File

@ -16,15 +16,27 @@ export class AddTaxItemTransaction1726045820711 implements MigrationInterface {
await queryRunner.query( await queryRunner.query(
`ALTER TABLE "transaction_item_breakdowns" ADD "total_profit_share" numeric`, `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( 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`, `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( 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`, `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<void> { public async down(queryRunner: QueryRunner): Promise<void> {
@ -34,9 +46,6 @@ export class AddTaxItemTransaction1726045820711 implements MigrationInterface {
await queryRunner.query( await queryRunner.query(
`ALTER TABLE "transaction_item_taxes" DROP CONSTRAINT "FK_f5c4966a381d903899cafb4b5ba"`, `ALTER TABLE "transaction_item_taxes" DROP CONSTRAINT "FK_f5c4966a381d903899cafb4b5ba"`,
); );
await queryRunner.query(
`ALTER TABLE "price_formulas" DROP COLUMN "value_for"`,
);
await queryRunner.query( await queryRunner.query(
`ALTER TABLE "transaction_item_breakdowns" DROP COLUMN "total_profit_share"`, `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 "t_breakdown_item_taxes"`);
await queryRunner.query(`DROP TABLE "transaction_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"`,
);
} }
} }

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFormulaToTax1726365023179 implements MigrationInterface {
name = 'AddFormulaToTax1726365023179';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "taxes" DROP COLUMN "formula_string"`);
await queryRunner.query(`ALTER TABLE "taxes" DROP COLUMN "formula_render"`);
}
}

View File

@ -6,12 +6,15 @@ import { SalesPriceFormulaModel } from '../models/sales-price-formula.model';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { FormulaType } from '../../constants'; import { FormulaType } from '../../constants';
import { TaxModel } from 'src/modules/transaction/tax/data/models/tax.model';
@Injectable() @Injectable()
export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceFormulaEntity> { export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceFormulaEntity> {
constructor( constructor(
@InjectRepository(SalesPriceFormulaModel, CONNECTION_NAME.DEFAULT) @InjectRepository(SalesPriceFormulaModel, CONNECTION_NAME.DEFAULT)
private repo: Repository<SalesPriceFormulaModel>, private repo: Repository<SalesPriceFormulaModel>,
@InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT)
private tax: Repository<TaxModel>,
) { ) {
super(repo); super(repo);
} }
@ -23,4 +26,23 @@ export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceForm
}, },
}); });
} }
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})`,
);
}
return salesFormula;
}
} }

View File

@ -9,22 +9,23 @@ import { apm } from 'src/core/apm';
export function calculateFormula( export function calculateFormula(
formula: string, formula: string,
variable: object[], variable: object,
total: number, total: number,
solveFor = 'dpp',
) { ) {
try { try {
const x1 = math.simplify(formula, variable).toString(); const x1 = math.simplify(formula, variable).toString();
console.log('Formula ', x1); // console.log('Formula ', x1);
const dppFormula = parse(x1); const dppFormula = parse(x1);
const totalFormula = parse(total.toString()); const totalFormula = parse(total.toString());
const equation = new Equation(totalFormula, dppFormula); const equation = new Equation(totalFormula, dppFormula);
console.log(equation.toString()); // console.log(equation.toString());
const result = equation.solveFor('dpp').toString(); const result = equation.solveFor(solveFor).toString();
console.log(result, 'formula'); // console.log(result, 'formula');
const value = math.evaluate(result); const value = math.evaluate(result);
console.log(value, 'value'); // console.log(value, 'value');
return value; return value;
} catch (e) { } catch (e) {

View File

@ -10,4 +10,10 @@ export class TaxModel extends BaseStatusModel<TaxEntity> implements TaxEntity {
@Column('float', { name: 'value', default: 0 }) @Column('float', { name: 'value', default: 0 })
value: number; value: number;
@Column('json', { name: 'formula_render', nullable: true })
formula_render: any;
@Column('varchar', { name: 'formula_string', nullable: true })
formula_string: string;
} }

View File

@ -29,7 +29,7 @@ export class TaxDataService extends BaseDataService<TaxEntity> {
for (const tax of taxes) { for (const tax of taxes) {
const { name, value } = tax; const { name, value } = tax;
keyVal[name] = value; keyVal[name] = value / 100;
} }
return keyVal; return keyVal;

View File

@ -3,4 +3,5 @@ import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.e
export interface TaxEntity extends BaseStatusEntity { export interface TaxEntity extends BaseStatusEntity {
name: string; name: string;
value: number; value: number;
formula_string?: string;
} }

View File

@ -68,6 +68,12 @@ export class TransactionItemModel
@Column('decimal', { name: 'total_profit_share', nullable: true }) @Column('decimal', { name: 'total_profit_share', nullable: true })
total_profit_share: number; 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 }) @Column('int', { name: 'qty', nullable: true })
qty: number; qty: number;
@ -129,6 +135,15 @@ export class TransactionItemBreakdownModel extends BaseCoreModel<TransactionBund
@Column('decimal', { nullable: true }) @Column('decimal', { nullable: true })
total_profit_share: number; total_profit_share: number;
@Column('decimal', { nullable: true })
total_share_tenant: number;
@Column('decimal', { nullable: true })
payment_total_dpp: number;
@Column('decimal', { nullable: true })
payment_total_tax: number;
@ManyToOne(() => TransactionItemModel, (model) => model.bundling_items, { @ManyToOne(() => TransactionItemModel, (model) => model.bundling_items, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',

View File

@ -34,4 +34,5 @@ export interface TransactionBundlingItemEntity extends BaseCoreEntity {
hpp: number; hpp: number;
base_price: number; base_price: number;
item_rates: number; item_rates: number;
total_price?: number;
} }

View File

@ -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 { TaxDataService } from 'src/modules/transaction/tax/data/services/tax-data.service';
import { TransactionEntity } from '../../entities/transaction.entity'; import { TransactionEntity } from '../../entities/transaction.entity';
import { calculateFormula } from 'src/modules/transaction/sales-price-formula/domain/usecases/managers/helpers/calculation-formula.helper'; 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() @Injectable()
export class PriceCalculator { export class PriceCalculator {
@ -11,56 +16,159 @@ export class PriceCalculator {
private taxService: TaxDataService, private taxService: TaxDataService,
) {} ) {}
private initialValue(tax: any) { private initialValue(formulas: any[]) {
if (typeof tax !== 'object' || tax === null) { const tax = {};
return null; // Return null for non-object values 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; return tax;
} }
async calculate(transaction: TransactionEntity) { async calculate(transaction: TransactionEntity) {
const taxes = await this.taxService.taxKeyValue(); const prices = [];
const formulas = await this.formulaService.profitShareFormula(); // for (const item of transaction.items) {
const values = this.initialValue(taxes); // 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( const dppValue = calculateFormula(
dpp.formula_string, dpp.formula_string,
taxes, taxes,
transaction.payment_total_net_profit, transaction.total_price,
); );
values['dpp'] = dppValue; values['dpp'] = dppValue;
values['dpp_value'] = dppValue;
let calledVariable = [];
do { do {
const valueFor = this.withNullValue(values); const valueFor = this.withNullValue(values, calledVariable);
const formula = formulas[valueFor]; const formula = taxFormula.find(
let result; (formula) => `${formula.name}_value` == valueFor,
);
let result = null;
try { try {
result = calculateFormula( result = math.evaluate(formula.formula_string, values);
formula.formula_string, calledVariable = [];
values, } catch (error) {
transaction.payment_total_net_profit, calledVariable.push(valueFor);
); console.log(error);
} catch (error) {} }
values[valueFor] = result; values[valueFor] = result;
values[`${valueFor}_value`] = result; // values[`${valueFor}_value`] = result;
} while (this.containsNullValue(values)); } 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 }; 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) { containsNullValue(obj) {
if (typeof obj !== 'object' || obj === null) { if (typeof obj !== 'object' || obj === null) {
return obj === null; // Return true if the value itself is 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 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) { if (typeof obj !== 'object' || obj === null) {
return null; // Return null for non-object values return null; // Return null for non-object values
} }

View File

@ -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 { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-read.orchestrator';
import { DetailTransactionManager } from './managers/detail-transaction.manager'; import { DetailTransactionManager } from './managers/detail-transaction.manager';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { PriceCalculator } from './calculator/price.calculator';
@Injectable() @Injectable()
export class TransactionReadOrchestrator extends BaseReadOrchestrator<TransactionEntity> { export class TransactionReadOrchestrator extends BaseReadOrchestrator<TransactionEntity> {
@ -13,6 +14,7 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator<Transactio
private indexManager: IndexTransactionManager, private indexManager: IndexTransactionManager,
private detailManager: DetailTransactionManager, private detailManager: DetailTransactionManager,
private serviceData: TransactionReadService, private serviceData: TransactionReadService,
private calculator: PriceCalculator,
) { ) {
super(); super();
} }
@ -30,4 +32,53 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator<Transactio
await this.detailManager.execute(); await this.detailManager.execute();
return this.detailManager.getResult(); return this.detailManager.getResult();
} }
async dummyCalculate(id: string): Promise<void> {
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<void> {
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);
}
}
}
} }

View File

@ -27,4 +27,18 @@ export class TransactionReadController {
async detail(@Param('id') id: string): Promise<TransactionEntity> { async detail(@Param('id') id: string): Promise<TransactionEntity> {
return await this.orchestrator.detail(id); return await this.orchestrator.detail(id);
} }
@Public(true)
@Get('dummy/:id')
async calculate(@Param('id') id: string): Promise<string> {
this.orchestrator.dummyCalculate(id);
return 'OK';
}
@Public(true)
@Get('dummy2/calculate')
async calculateAll(): Promise<string> {
this.orchestrator.calculatePrice();
return 'OK';
}
} }

View File

@ -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 { PaymentMethodDataService } from '../payment-method/data/services/payment-method-data.service';
import { PaymentMethodModel } from '../payment-method/data/models/payment-method.model'; import { PaymentMethodModel } from '../payment-method/data/models/payment-method.model';
import { TransactionDemographyModel } from './data/models/transaction-demography.model'; import { TransactionDemographyModel } from './data/models/transaction-demography.model';
import { PriceCalculator } from './domain/usecases/calculator/price.calculator';
@Module({ @Module({
exports: [TransactionReadService], exports: [TransactionReadService],
@ -61,6 +62,7 @@ import { TransactionDemographyModel } from './data/models/transaction-demography
], ],
controllers: [TransactionDataController, TransactionReadController], controllers: [TransactionDataController, TransactionReadController],
providers: [ providers: [
PriceCalculator,
RefundUpdatedHandler, RefundUpdatedHandler,
PosTransactionHandler, PosTransactionHandler,
MidtransCallbackHandler, MidtransCallbackHandler,