fix: formula calculation
continuous-integration/drone/push Build is passing Details

pull/93/head
shancheas 2024-09-19 19:33:28 +07:00
parent 23043fb7f9
commit e09c76309e
9 changed files with 182 additions and 40 deletions

View File

@ -45,8 +45,10 @@ import { GoogleCalendarModule } from './modules/configuration/google-calendar/go
import { TransactionModule } from './modules/transaction/transaction/transaction.module'; import { TransactionModule } from './modules/transaction/transaction/transaction.module';
import { TransactionModel } from './modules/transaction/transaction/data/models/transaction.model'; import { TransactionModel } from './modules/transaction/transaction/data/models/transaction.model';
import { import {
TransactionBreakdownTaxModel,
TransactionItemBreakdownModel, TransactionItemBreakdownModel,
TransactionItemModel, TransactionItemModel,
TransactionItemTaxModel,
} from './modules/transaction/transaction/data/models/transaction-item.model'; } from './modules/transaction/transaction/data/models/transaction-item.model';
import { TransactionTaxModel } from './modules/transaction/transaction/data/models/transaction-tax.model'; import { TransactionTaxModel } from './modules/transaction/transaction/data/models/transaction-tax.model';
import { ReconciliationModule } from './modules/transaction/reconciliation/reconciliation.module'; import { ReconciliationModule } from './modules/transaction/reconciliation/reconciliation.module';
@ -121,6 +123,8 @@ import { AuthService } from './core/guards/domain/services/auth.service';
TransactionTaxModel, TransactionTaxModel,
TransactionDemographyModel, TransactionDemographyModel,
TransactionItemBreakdownModel, TransactionItemBreakdownModel,
TransactionItemTaxModel,
TransactionBreakdownTaxModel,
UserModel, UserModel,
UserLoginModel, UserLoginModel,

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; import { BaseDataService } from 'src/core/modules/data/service/base-data.service';
import { SalesPriceFormulaEntity } from '../../domain/entities/sales-price-formula.entity'; import { SalesPriceFormulaEntity } from '../../domain/entities/sales-price-formula.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
@ -7,6 +7,7 @@ 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'; import { TaxModel } from 'src/modules/transaction/tax/data/models/tax.model';
import { ItemModel } from 'src/modules/item-related/item/data/models/item.model';
@Injectable() @Injectable()
export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceFormulaEntity> { export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceFormulaEntity> {
@ -15,6 +16,8 @@ export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceForm
private repo: Repository<SalesPriceFormulaModel>, private repo: Repository<SalesPriceFormulaModel>,
@InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT) @InjectRepository(TaxModel, CONNECTION_NAME.DEFAULT)
private tax: Repository<TaxModel>, private tax: Repository<TaxModel>,
@InjectRepository(ItemModel, CONNECTION_NAME.DEFAULT)
private item: Repository<ItemModel>,
) { ) {
super(repo); super(repo);
} }
@ -27,6 +30,20 @@ export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceForm
}); });
} }
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() { async salesPriceFormula() {
const salesFormula = await this.repo.findOne({ const salesFormula = await this.repo.findOne({
where: { where: {
@ -43,6 +60,32 @@ export class SalesPriceFormulaDataService extends BaseDataService<SalesPriceForm
); );
} }
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; return salesFormula;
} }
} }

View File

@ -30,7 +30,15 @@ export function calculateFormula(
return value; return value;
} catch (e) { } catch (e) {
apm.captureError(e); apm.captureError(e);
throw new BadRequestException('Wrong value'); throw new BadRequestException({
message: 'Wrong value',
meta: {
formula,
variable,
total,
solveFor,
},
});
} }
} }

View File

@ -14,13 +14,14 @@ import { DetailSalesPriceFormulaManager } from './domain/usecases/managers/detai
import { SalesPriceFormulaModel } from './data/models/sales-price-formula.model'; import { SalesPriceFormulaModel } from './data/models/sales-price-formula.model';
import { TaxDataService } from '../tax/data/services/tax-data.service'; import { TaxDataService } from '../tax/data/services/tax-data.service';
import { TaxModel } from '../tax/data/models/tax.model'; import { TaxModel } from '../tax/data/models/tax.model';
import { ItemModel } from 'src/modules/item-related/item/data/models/item.model';
@Global() @Global()
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot(),
TypeOrmModule.forFeature( TypeOrmModule.forFeature(
[SalesPriceFormulaModel, TaxModel], [SalesPriceFormulaModel, TaxModel, ItemModel],
CONNECTION_NAME.DEFAULT, CONNECTION_NAME.DEFAULT,
), ),
CqrsModule, CqrsModule,

View File

@ -113,6 +113,13 @@ export class TransactionItemModel
}, },
) )
bundling_items: TransactionItemBreakdownModel[]; bundling_items: TransactionItemBreakdownModel[];
@OneToMany(() => TransactionItemTaxModel, (model) => model.transaction, {
cascade: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
item_taxes: TransactionItemTaxModel[];
} }
@Entity(TABLE_NAME.TRANSACTION_ITEM_BREAKDOWN) @Entity(TABLE_NAME.TRANSACTION_ITEM_BREAKDOWN)
@ -150,6 +157,13 @@ export class TransactionItemBreakdownModel extends BaseCoreModel<TransactionBund
}) })
@JoinColumn({ name: 'transaction_item_id' }) @JoinColumn({ name: 'transaction_item_id' })
transaction_item: TransactionItemModel; transaction_item: TransactionItemModel;
@OneToMany(() => TransactionBreakdownTaxModel, (model) => model.transaction, {
cascade: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
item_taxes: TransactionBreakdownTaxModel[];
} }
@Entity(TABLE_NAME.TRANSACTION_ITEM_TAX) @Entity(TABLE_NAME.TRANSACTION_ITEM_TAX)

View File

@ -6,6 +6,7 @@ import {
} from '../../constants'; } from '../../constants';
import { STATUS } from 'src/core/strings/constants/base.constants'; import { STATUS } from 'src/core/strings/constants/base.constants';
import { TransactionItemEntity } from './transaction-item.entity'; import { TransactionItemEntity } from './transaction-item.entity';
import { TransactionTaxEntity } from './transaction-tax.entity';
export interface TransactionEntity extends BaseStatusEntity { export interface TransactionEntity extends BaseStatusEntity {
// general info // general info
@ -89,4 +90,5 @@ export interface TransactionEntity extends BaseStatusEntity {
calendar_link?: string; calendar_link?: string;
items: TransactionItemEntity[]; items: TransactionItemEntity[];
taxes?: TransactionTaxEntity[];
} }

View File

@ -27,18 +27,14 @@ export class PriceCalculator {
async calculate(transaction: TransactionEntity) { async calculate(transaction: TransactionEntity) {
const prices = []; const prices = [];
// for (const item of transaction.items) { const transaction_taxes = [];
// const price = await this.calculateItem(item);
// const priceValues = this.calculatePrice(price);
// prices.push(priceValues);
// }
for (let i = 0; i < transaction.items.length; i++) { for (let i = 0; i < transaction.items.length; i++) {
const item = transaction.items[i]; const item = transaction.items[i];
const price = await this.calculateItem(item); const price = await this.calculateItem(item);
const priceValues = this.calculatePrice(price); const priceValues = this.calculatePrice(price);
prices.push(priceValues); prices.push(priceValues);
transaction_taxes.push(price.taxesValue);
if (item.bundling_items) { if (item.bundling_items) {
for (let b = 0; b < item.bundling_items.length; b++) { for (let b = 0; b < item.bundling_items.length; b++) {
@ -48,40 +44,66 @@ export class PriceCalculator {
const bundlingPrice = await this.calculateItem(bundling); const bundlingPrice = await this.calculateItem(bundling);
const bundlingValues = this.calculatePrice(bundlingPrice); const bundlingValues = this.calculatePrice(bundlingPrice);
Object.assign(bundling, bundlingValues); const item_taxes = { item_taxes: bundlingPrice.taxesValue };
Object.assign(bundling, bundlingValues, item_taxes);
item.bundling_items[b] = bundling; item.bundling_items[b] = bundling;
} }
} }
Object.assign(item, priceValues); const item_taxes = { item_taxes: price.taxesValue };
Object.assign(item, priceValues, item_taxes);
transaction.items[i] = item; transaction.items[i] = item;
} }
const tatal_taxes = this.mergeAndSumArrays(transaction_taxes);
transaction.taxes = tatal_taxes;
const { payment_total_dpp, ...otherValue } = const { payment_total_dpp, ...otherValue } =
this.sumArrayBasedOnObjectKey(prices); this.sumArrayBasedOnObjectKey(prices);
return { dpp_value: payment_total_dpp, other: otherValue, tax_datas: [] }; return {
dpp_value: payment_total_dpp,
other: otherValue,
tax_datas: tatal_taxes,
};
} }
async calculateItem( async calculateItem(
transaction: TransactionItemEntity | TransactionBundlingItemEntity, transaction: TransactionItemEntity | TransactionBundlingItemEntity,
) { ) {
const itemShare = 20 / 100; // const itemShare = 20 / 100;
const profitShare = 10 / 100; // const profitShare = 10 / 100;
const { profitShare, tenantShare } = await this.formulaService.itemTax(
transaction.item_id,
);
const tax = await this.taxService.taxKeyValue(); const tax = await this.taxService.taxKeyValue();
const taxes = { ...tax, item_share: itemShare, profit_share: profitShare }; /* Update constant from
const formulas = await this.formulaService.salesPriceFormula(); - 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 taxFormula = await this.taxService.getManyByOptions({});
const shareFormulas = await this.formulaService.profitShareFormula(); const shareFormulas = await this.formulaService.profitShareFormula();
const taxShareFormulas = shareFormulas.map((formula) => {
return {
...formula,
name: formula.value_for,
};
});
const formulas = [...taxFormula, ...taxShareFormulas];
const values = { const values = {
total: transaction.total_price, total: transaction.total_price,
...this.initialValue(taxFormula), ...this.initialValue(formulas),
...taxes, ...taxes,
}; };
// const dpp = formulas.find((formula) => formula.value_for == 'dpp'); // const dpp = formulas.find((formula) => formula.value_for == 'dpp');
const dpp = formulas;
const dppValue = calculateFormula( const dppValue = calculateFormula(
dpp.formula_string, dpp.formula_string,
@ -93,11 +115,14 @@ export class PriceCalculator {
values['dpp_value'] = dppValue; values['dpp_value'] = dppValue;
let calledVariable = []; let calledVariable = [];
const taxesValue = [];
do { do {
const valueFor = this.withNullValue(values, calledVariable); const valueFor = this.withNullValue(values, calledVariable);
const formula = taxFormula.find(
(formula) => `${formula.name}_value` == valueFor, const formula = formulas.find((formula) => {
); const name = formula['name'] ?? formula['value_for'];
return `${name}_value` == valueFor;
});
let result = null; let result = null;
try { try {
@ -109,26 +134,38 @@ export class PriceCalculator {
} }
values[valueFor] = result; 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; // values[`${valueFor}_value`] = result;
} while (this.containsNullValue(values)); } while (this.containsNullValue(values));
const itemShareFormula = shareFormulas.find( // const itemShareFormula = shareFormulas.find(
(f) => f.value_for == 'item_share', // (f) => f.value_for == 'tenant_share',
); // );
values['item_share_value'] = math.evaluate( // values['tenant_share_value'] = math.evaluate(
itemShareFormula.formula_string, // itemShareFormula.formula_string,
values, // values,
); // );
const profitShareFormula = shareFormulas.find( // const profitShareFormula = shareFormulas.find(
(f) => f.value_for == 'profit_share', // (f) => f.value_for == 'profit_share',
); // );
values['profit_share_value'] = math.evaluate( // values['profit_share_value'] = math.evaluate(
profitShareFormula.formula_string, // profitShareFormula.formula_string,
values, // values,
); // );
return { dpp_value: dppValue, tax_datas: values }; return { dpp_value: dppValue, tax_datas: values, taxesValue };
} }
calculatePrice(prices: any) { calculatePrice(prices: any) {
@ -137,14 +174,15 @@ export class PriceCalculator {
.filter((key) => key.endsWith('_value')) .filter((key) => key.endsWith('_value'))
.reduce((acc, key) => ({ ...acc, [key]: data[key] }), {}); .reduce((acc, key) => ({ ...acc, [key]: data[key] }), {});
const { dpp_value, item_share_value, profit_share_value, ...tax } = const { dpp_value, tenant_share_value, profit_share_value, ...tax } =
filteredObject; filteredObject;
const taxes = this.sumPriceObject(tax); const taxes = this.sumPriceObject(tax);
return { return {
total_profit_share: item_share_value, total_profit_share: profit_share_value,
total_share_tenant: profit_share_value, total_share_tenant: tenant_share_value,
payment_total_tax: taxes, payment_total_tax: taxes,
payment_total_dpp: dpp_value, payment_total_dpp: dpp_value,
tax,
}; };
} }
@ -185,6 +223,28 @@ export class PriceCalculator {
return false; // If no null values are found, return false 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 = []) { withNullValue(mainObj, called = []) {
const obj = { ...mainObj }; const obj = { ...mainObj };
for (const variable of called) { for (const variable of called) {

View File

@ -64,7 +64,9 @@ export class SettledTransactionHandler
}); });
// const profit_share_value = this.calculateFormula(profit_formula.formula_string, taxes, data.payment_total_net_profit ?? 0); // const profit_share_value = this.calculateFormula(profit_formula.formula_string, taxes, data.payment_total_net_profit ?? 0);
const { dpp_value, tax_datas } = await this.calculator.calculate(data); const { dpp_value, tax_datas, other } = await this.calculator.calculate(
data,
);
// calculateSalesFormula( // calculateSalesFormula(
// sales_price.formula_string, // sales_price.formula_string,
@ -82,6 +84,8 @@ export class SettledTransactionHandler
taxes: tax_datas, taxes: tax_datas,
calendar_id: google_calendar?.id, calendar_id: google_calendar?.id,
calendar_link: google_calendar?.htmlLink, calendar_link: google_calendar?.htmlLink,
payment_total_share: other.total_profit_share,
payment_total_tax: other.payment_total_tax,
}); });
} else if (oldSettled) { } else if (oldSettled) {
// console.log(data, 'data oldSettled'); // console.log(data, 'data oldSettled');

View File

@ -19,8 +19,10 @@ import { BatchDeleteTransactionManager } from './domain/usecases/managers/batch-
import { BatchConfirmTransactionManager } from './domain/usecases/managers/batch-confirm-transaction.manager'; import { BatchConfirmTransactionManager } from './domain/usecases/managers/batch-confirm-transaction.manager';
import { TransactionModel } from './data/models/transaction.model'; import { TransactionModel } from './data/models/transaction.model';
import { import {
TransactionBreakdownTaxModel,
TransactionItemBreakdownModel, TransactionItemBreakdownModel,
TransactionItemModel, TransactionItemModel,
TransactionItemTaxModel,
} from './data/models/transaction-item.model'; } from './data/models/transaction-item.model';
import { TransactionTaxModel } from './data/models/transaction-tax.model'; import { TransactionTaxModel } from './data/models/transaction-tax.model';
import { CancelTransactionManager } from './domain/usecases/managers/cancel-transaction.manager'; import { CancelTransactionManager } from './domain/usecases/managers/cancel-transaction.manager';
@ -40,6 +42,7 @@ import { PaymentMethodDataService } from '../payment-method/data/services/paymen
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'; import { PriceCalculator } from './domain/usecases/calculator/price.calculator';
import { ItemModel } from 'src/modules/item-related/item/data/models/item.model';
@Module({ @Module({
exports: [TransactionReadService], exports: [TransactionReadService],
@ -52,9 +55,12 @@ import { PriceCalculator } from './domain/usecases/calculator/price.calculator';
TransactionDemographyModel, TransactionDemographyModel,
TransactionItemBreakdownModel, TransactionItemBreakdownModel,
TransactionTaxModel, TransactionTaxModel,
TransactionItemTaxModel,
TransactionBreakdownTaxModel,
TaxModel, TaxModel,
SalesPriceFormulaModel, SalesPriceFormulaModel,
PaymentMethodModel, PaymentMethodModel,
ItemModel,
], ],
CONNECTION_NAME.DEFAULT, CONNECTION_NAME.DEFAULT,
), ),