From 3163f835dd9a310087852c47c6b3ee804c345e46 Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:54:14 +0530 Subject: [PATCH] feat: create LoyaltyPointEntry --- models/baseModels/Invoice/Invoice.ts | 48 +++++++++++++++ models/baseModels/Party/Party.ts | 34 ++++++++++ models/helpers.ts | 92 ++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index db98d386..0f3054ff 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -14,7 +14,9 @@ import { Transactional } from 'models/Transactional/Transactional'; import { addItem, canApplyPricingRule, + createLoyaltyPointEntry, filterPricingRules, + getAddedLPWithGrandTotal, getExchangeRate, getNumberSeries, getPricingRulesConflicts, @@ -38,6 +40,7 @@ import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types'; import { PricingRule } from '../PricingRule/PricingRule'; import { ApplicablePricingRules } from './types'; import { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail'; +import { LoyaltyProgram } from '../LoyaltyProgram/LoyaltyProgram'; export type TaxDetail = { account: string; @@ -69,8 +72,10 @@ export abstract class Invoice extends Transactional { setDiscountAmount?: boolean; discountAmount?: Money; discountPercent?: number; + loyaltyPoints?: number; discountAfterTax?: boolean; stockNotTransferred?: number; + loyaltyProgram?: string; backReference?: string; submitted?: boolean; @@ -207,6 +212,7 @@ export abstract class Invoice extends Transactional { } await this._updateIsItemsReturned(); + await this._createLoyaltyPointEntry(); } async afterCancel() { @@ -538,6 +544,28 @@ export abstract class Invoice extends Transactional { await invoiceDoc.submit(); } + async _createLoyaltyPointEntry() { + if (!this.loyaltyProgram) { + return; + } + + const loyaltyProgramDoc = (await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + )) as LoyaltyProgram; + + const expiryDate = this.date as Date; + const fromDate = loyaltyProgramDoc.fromDate as Date; + const toDate = loyaltyProgramDoc.toDate as Date; + + if (fromDate <= expiryDate && toDate >= expiryDate) { + const party = (await this.loadAndGetLink('party')) as Party; + + await createLoyaltyPointEntry(this); + await party.updateLoyaltyPoints(); + } + } + async _validateHasLinkedReturnInvoices() { if (!this.name || this.isReturn || this.isQuote) { return; @@ -560,6 +588,16 @@ export abstract class Invoice extends Transactional { ); } + async getLPAddedBaseGrandTotal() { + const totalLotaltyAmount = await getAddedLPWithGrandTotal( + this.fyo, + this.loyaltyProgram as string, + this.loyaltyPoints as number + ); + + return totalLotaltyAmount.sub(this.baseGrandTotal as Money).abs(); + } + formulas: FormulaMap = { account: { formula: async () => { @@ -571,6 +609,16 @@ export abstract class Invoice extends Transactional { }, dependsOn: ['party'], }, + loyaltyProgram: { + formula: async () => { + const partyDoc = await this.fyo.doc.getDoc( + ModelNameEnum.Party, + this.party + ); + return partyDoc?.loyaltyProgram as string; + }, + dependsOn: ['party', 'name'], + }, currency: { formula: async () => { const currency = (await this.fyo.getValue( diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index 082ef6a6..0636be6e 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -54,6 +54,40 @@ export class Party extends Doc { await this.setAndSync({ outstandingAmount }); } + async updateLoyaltyPoints() { + let loyaltyPoints = 0; + + if (this.role === 'Customer' || this.role === 'Both') { + loyaltyPoints = await this._getTotalLoyaltyPoints(); + } + + await this.setAndSync({ loyaltyPoints }); + } + + async _getTotalLoyaltyPoints() { + const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, { + fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'], + filters: { + customer: this.name as string, + }, + })) as { + name: string; + loyaltyPoints: number; + expiryDate: Date; + postingDate: Date; + }[]; + + const totalLoyaltyPoints = data.reduce((total, entry) => { + if (entry.expiryDate > entry.postingDate) { + return total + entry.loyaltyPoints; + } + + return total; + }, 0); + + return totalLoyaltyPoints; + } + async _getTotalOutstandingAmount( schemaName: 'SalesInvoice' | 'PurchaseInvoice' ) { diff --git a/models/helpers.ts b/models/helpers.ts index eb634cf2..1d93bd5d 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -25,6 +25,9 @@ import { Lead } from './baseModels/Lead/Lead'; import { PricingRule } from './baseModels/PricingRule/PricingRule'; import { ValidationError } from 'fyo/utils/errors'; import { ApplicablePricingRules } from './baseModels/Invoice/types'; +import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram'; +import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems'; +import { isPesa } from 'fyo/utils'; export function getQuoteActions( fyo: Fyo, @@ -663,6 +666,95 @@ export async function addItem(name: string, doc: M) { await item.set('item', name); } +export async function createLoyaltyPointEntry(doc: Invoice) { + const loyaltyProgramDoc = (await doc.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + doc?.loyaltyProgram + )) as LoyaltyProgram; + + if (!loyaltyProgramDoc.isEnabled) { + return; + } + const expiryDate = new Date(Date.now()); + + expiryDate.setDate( + expiryDate.getDate() + (loyaltyProgramDoc.expiryDuration || 0) + ); + + const loyaltyProgramTier = getLoyaltyProgramTier( + loyaltyProgramDoc, + doc?.grandTotal as Money + ) as CollectionRulesItems; + + if (!loyaltyProgramTier) { + return; + } + + const collectionFactor = loyaltyProgramTier.collectionFactor as number; + const loyaltyPoint = + Math.round(doc?.grandTotal?.float || 0) * collectionFactor; + + const newLoyaltyPointEntry = doc.fyo.doc.getNewDoc( + ModelNameEnum.LoyaltyPointEntry, + { + loyaltyProgram: doc.loyaltyProgram, + customer: doc.party, + invoice: doc.name, + postingDate: doc.date, + purchaseAmount: doc.grandTotal, + expiryDate: expiryDate, + loyaltyProgramTier: loyaltyProgramTier?.tierName, + loyaltyPoints: loyaltyPoint, + } + ); + + return await newLoyaltyPointEntry.sync(); +} + +export async function getAddedLPWithGrandTotal( + fyo: Fyo, + loyaltyProgram: string, + loyaltyPoints: number +) { + const loyaltyProgramDoc = (await fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + loyaltyProgram + )) as LoyaltyProgram; + + const conversionFactor = loyaltyProgramDoc.conversionFactor as number; + + return fyo.pesa((loyaltyPoints || 0) * conversionFactor); +} + +export function getLoyaltyProgramTier( + loyaltyProgramData: LoyaltyProgram, + grandTotal: Money +): CollectionRulesItems | undefined { + if (!loyaltyProgramData.collectionRules) { + return; + } + + let loyaltyProgramTier: CollectionRulesItems | undefined; + + for (const row of loyaltyProgramData.collectionRules) { + if (isPesa(row.minimumTotalSpent)) { + const minimumSpent = row.minimumTotalSpent; + + if (!minimumSpent.lte(grandTotal)) { + continue; + } + + if ( + !loyaltyProgramTier || + minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money) + ) { + loyaltyProgramTier = row; + } + } + } + return loyaltyProgramTier; +} + export async function getPricingRule( doc: Invoice ): Promise {