feat: price calculator
parent
19494b3328
commit
614a9346fb
|
@ -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<void> {
|
||||
|
@ -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"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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<SalesPriceFormulaEntity> {
|
||||
constructor(
|
||||
@InjectRepository(SalesPriceFormulaModel, CONNECTION_NAME.DEFAULT)
|
||||
private repo: Repository<SalesPriceFormulaModel>,
|
||||
@InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT)
|
||||
private tax: Repository<TaxModel>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,22 +9,23 @@ import { apm } from 'src/core/apm';
|
|||
|
||||
export function calculateFormula(
|
||||
formula: string,
|
||||
variable: object[],
|
||||
variable: object,
|
||||
total: number,
|
||||
solveFor = 'dpp',
|
||||
) {
|
||||
try {
|
||||
const x1 = math.simplify(formula, variable).toString();
|
||||
console.log('Formula ', x1);
|
||||
// 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('dpp').toString();
|
||||
console.log(result, 'formula');
|
||||
// console.log(equation.toString());
|
||||
const result = equation.solveFor(solveFor).toString();
|
||||
// console.log(result, 'formula');
|
||||
|
||||
const value = math.evaluate(result);
|
||||
console.log(value, 'value');
|
||||
// console.log(value, 'value');
|
||||
|
||||
return value;
|
||||
} catch (e) {
|
||||
|
|
|
@ -10,4 +10,10 @@ export class TaxModel extends BaseStatusModel<TaxEntity> 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;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class TaxDataService extends BaseDataService<TaxEntity> {
|
|||
|
||||
for (const tax of taxes) {
|
||||
const { name, value } = tax;
|
||||
keyVal[name] = value;
|
||||
keyVal[name] = value / 100;
|
||||
}
|
||||
|
||||
return keyVal;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<TransactionBund
|
|||
@Column('decimal', { nullable: true })
|
||||
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, {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
|
|
|
@ -34,4 +34,5 @@ export interface TransactionBundlingItemEntity extends BaseCoreEntity {
|
|||
hpp: number;
|
||||
base_price: number;
|
||||
item_rates: number;
|
||||
total_price?: number;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<TransactionEntity> {
|
||||
|
@ -13,6 +14,7 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator<Transactio
|
|||
private indexManager: IndexTransactionManager,
|
||||
private detailManager: DetailTransactionManager,
|
||||
private serviceData: TransactionReadService,
|
||||
private calculator: PriceCalculator,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
@ -30,4 +32,53 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator<Transactio
|
|||
await this.detailManager.execute();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,4 +27,18 @@ export class TransactionReadController {
|
|||
async detail(@Param('id') id: string): Promise<TransactionEntity> {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue