diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts new file mode 100644 index 00000000..ab5eb70c --- /dev/null +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -0,0 +1,45 @@ +import { DatabaseManager } from '../backend/database/manager'; +import { ModelNameEnum } from '../models/types'; + +export async function checkLoyaltyProgramExpiry() { + const dm = new DatabaseManager(); + + try { + const currentDate = new Date(); + + const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'], + filters: { + status: ['not in', ['Expired']], + isEnabled: true, + }, + }); + + if (loyaltyPrograms) { + for (const program of loyaltyPrograms) { + if (program.toDate && new Date(String(program.toDate)) <= currentDate) { + await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) + .where({ name: program.name }) + .update({ + status: 'Expired', + isEnabled: false, + }); + } + } + } + + const result = { + timestamp: currentDate.toISOString(), + }; + + return result; + } catch (error) { + throw error; + } finally { + await dm.call('close'); + } +} + +checkLoyaltyProgramExpiry().catch((error) => { + throw error; +}); diff --git a/main/initSheduler.ts b/main/initSheduler.ts index 9a4a8551..ea80c2a0 100644 --- a/main/initSheduler.ts +++ b/main/initSheduler.ts @@ -24,6 +24,15 @@ export async function initScheduler(interval: string) { }, }, }, + { + name: 'checkLoyaltyProgramExpiry', + interval: '0 1 * * *', + worker: { + workerData: { + useTsNode: true, + }, + }, + }, ], worker: { argv: ['--require', 'ts-node/register'], diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 2fa9b2d5..049470af 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -28,6 +28,9 @@ import { getReturnLoyaltyPoints, getItemQtyMap, getItemVisibility, + validateLoyaltyProgram, + getLoyaltyProgramTier, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -212,13 +215,49 @@ export abstract class Invoice extends Transactional { this.party )) as Party; - if ((this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0)) { + if (this.redeemLoyaltyPoints && (this.loyaltyPoints as number) > 0) { + const currentPoints = partyDoc?.loyaltyPoints || 0; + + let pointsToBeEarned = 0; + if (!this.isReturn && this.loyaltyProgram) { + const loyaltyProgramDoc = (await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + )) as LoyaltyProgram; + + const tier = getLoyaltyProgramTier( + loyaltyProgramDoc, + this?.grandTotal as Money + ); + + if (tier) { + const collectionFactor = tier.collectionFactor as number; + pointsToBeEarned = + Math.round(this?.grandTotal?.float || 0) * collectionFactor; + } + } + + const totalAvailablePoints = currentPoints + pointsToBeEarned; + if ((this.loyaltyPoints as number) > totalAvailablePoints) { + throw new ValidationError( + t`${ + this.party as string + } only has ${currentPoints} points (${pointsToBeEarned} will be earned from this transaction)` + ); + } + } else if ( + (this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0) + ) { throw new ValidationError( t`${this.party as string} only has ${ partyDoc.loyaltyPoints as number } points` ); } + + if (this.loyaltyProgram) { + await validateLoyaltyProgram(this, this.loyaltyProgram); + } } async afterSubmit() { @@ -275,6 +314,10 @@ export abstract class Invoice extends Transactional { if (this.schemaName === ModelNameEnum.SalesInvoice) { this.updateUsedCountOfCoupons(); } + + if (this.loyaltyProgram) { + await this.updateUsedCountOfLoyaltyProgram(); + } } async afterCancel() { @@ -284,6 +327,10 @@ export abstract class Invoice extends Transactional { await this._updateIsItemsReturned(); await this._removeLoyaltyPointEntry(); this.reduceUsedCountOfCoupons(); + + if (this.loyaltyProgram) { + await this.reduceUsedCountOfLoyaltyProgram(); + } } async _removeLoyaltyPointEntry() { @@ -838,6 +885,63 @@ export abstract class Invoice extends Transactional { }); } + async updateUsedCountOfLoyaltyProgram() { + if (!this.loyaltyProgram) { + return; + } + + const loyaltyProgramDoc = await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + ); + + const maximumUse = loyaltyProgramDoc.maximumUse as number; + const used = (loyaltyProgramDoc.used as number) || 0; + + if (this.redeemLoyaltyPoints) { + const newUsedCount = used + 1; + + if (maximumUse > 0 && newUsedCount >= maximumUse) { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + isEnabled: false, + }); + } else { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + }); + } + } + } + + async reduceUsedCountOfLoyaltyProgram() { + if (!this.loyaltyProgram) { + return; + } + + const loyaltyProgramDoc = await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + ); + + const maximumUse = loyaltyProgramDoc.maximumUse as number; + const used = (loyaltyProgramDoc.used as number) || 0; + const newUsedCount = used - 1; + + if (this.redeemLoyaltyPoints) { + if (newUsedCount < maximumUse) { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + isEnabled: true, + }); + } else { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + }); + } + } + } + async updateIsItemsFullyReturned(doc?: Invoice) { if (!doc?.returnAgainst || doc.schemaName !== ModelNameEnum.SalesInvoice) { return; @@ -898,11 +1002,27 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram )) as LoyaltyProgram; - const expiryDate = this.date as Date; + const invoiceDate = this.date as Date; const fromDate = loyaltyProgramDoc.fromDate as Date; const toDate = loyaltyProgramDoc.toDate as Date; - if (fromDate <= expiryDate && toDate >= expiryDate) { + const normalizedInvoiceDate = new Date(invoiceDate); + normalizedInvoiceDate.setHours(0, 0, 0, 0); + + const normalizedFromDate = new Date(fromDate); + normalizedFromDate.setHours(0, 0, 0, 0); + + const normalizedToDate = new Date(toDate); + normalizedToDate.setHours(0, 0, 0, 0); + + if (normalizedToDate.getTime() < normalizedInvoiceDate.getTime()) { + return; + } + + if ( + normalizedInvoiceDate.getTime() >= normalizedFromDate.getTime() && + normalizedInvoiceDate.getTime() <= normalizedToDate.getTime() + ) { const party = (await this.loadAndGetLink('party')) as Party; await createLoyaltyPointEntry(this); @@ -989,7 +1109,13 @@ export abstract class Invoice extends Transactional { ModelNameEnum.Party, this.party ); - return partyDoc?.loyaltyProgram as string; + const loyaltyProgramName = partyDoc?.loyaltyProgram as string; + + if (!loyaltyProgramName) { + return ''; + } + + return loyaltyProgramName; }, dependsOn: ['party', 'name'], }, @@ -999,6 +1125,17 @@ export abstract class Invoice extends Transactional { return 0; } + const loyaltyProgramName = this.loyaltyProgram as string; + if (loyaltyProgramName) { + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( + this.fyo, + loyaltyProgramName + ); + if (isExpiredAndMaxed) { + return 0; + } + } + const loyaltyPoints = await this.fyo.getValue( ModelNameEnum.Party, this.party, @@ -1006,7 +1143,7 @@ export abstract class Invoice extends Transactional { ); return loyaltyPoints || 0; }, - dependsOn: ['party'], + dependsOn: ['party', 'loyaltyProgram'], }, currency: { formula: async () => { @@ -1199,7 +1336,13 @@ export abstract class Invoice extends Transactional { loyaltyProgram: () => !this.loyaltyProgram, availableLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn, loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn, - redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn, + redeemLoyaltyPoints: () => { + if (!this.loyaltyProgram || this.isReturn) { + return true; + } + + return (this.availableLoyaltyPoints ?? 0) <= 0; + }, coupons: () => this.isSubmitted && !this.coupons?.length, priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList || diff --git a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts index 89f73399..20d8db45 100644 --- a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts +++ b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts @@ -1,22 +1,42 @@ +import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FiltersMap, ListViewSettings } from 'fyo/model/types'; +import { ListViewSettings, ValidationMap } from 'fyo/model/types'; +import { ValidationError } from 'fyo/utils/errors'; import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems'; -import { AccountRootTypeEnum } from '../Account/types'; +import { getLoyaltyProgramStatusColumn } from '../../helpers'; export class LoyaltyProgram extends Doc { collectionRules?: CollectionRulesItems[]; expiryDuration?: number; + maximumUse?: number; + used?: number; + status?: 'Active' | 'Expired' | 'Disabled' | 'Maxed'; - static filters: FiltersMap = { - expenseAccount: () => ({ - rootType: AccountRootTypeEnum.Expense, - isGroup: false, - }), + validations: ValidationMap = { + used: (value: DocValue) => { + const used = value as number; + const maximumUse = this.maximumUse as number; + + if (used < 0) { + throw new ValidationError('Used count cannot be negative'); + } + + if (maximumUse > 0 && used > maximumUse) { + throw new ValidationError('Used count cannot exceed maximum use limit'); + } + }, + maximumUse: (value: DocValue) => { + const maximumUse = value as number; + + if (maximumUse < 0) { + throw new ValidationError('Maximum use cannot be negative'); + } + }, }; static getListViewSettings(): ListViewSettings { return { - columns: ['name', 'fromDate', 'toDate', 'expiryDuration'], + columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'], }; } } diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index bba7181f..e277bd0d 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -14,6 +14,7 @@ import { import { Money } from 'pesa'; import { PartyRole } from './types'; import { ModelNameEnum } from 'models/types'; +import { isLoyaltyProgramExpiredAndMaxed } from 'models/helpers'; export class Party extends Doc { role?: PartyRole; @@ -66,6 +67,17 @@ export class Party extends Doc { } async _getTotalLoyaltyPoints() { + const loyaltyProgramName = this.loyaltyProgram as string; + if (loyaltyProgramName) { + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( + this.fyo, + loyaltyProgramName + ); + if (isExpiredAndMaxed) { + return 0; + } + } + const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, { fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'], filters: { diff --git a/models/baseModels/SalesInvoice/SalesInvoice.ts b/models/baseModels/SalesInvoice/SalesInvoice.ts index f4d94a1d..642712dc 100644 --- a/models/baseModels/SalesInvoice/SalesInvoice.ts +++ b/models/baseModels/SalesInvoice/SalesInvoice.ts @@ -113,6 +113,15 @@ export class SalesInvoice extends Invoice { ModelNameEnum.LoyaltyProgram, this.loyaltyProgram )) as LoyaltyProgram; + const toDate = loyaltyProgramDoc?.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + throw new ValidationError( + t`Loyalty program has expired and cannot be applied` + ); + } if (!this?.grandTotal) { return; diff --git a/models/baseModels/tests/testLoyaltyProgram.spec.ts b/models/baseModels/tests/testLoyaltyProgram.spec.ts index 2be654b8..8a1b9657 100644 --- a/models/baseModels/tests/testLoyaltyProgram.spec.ts +++ b/models/baseModels/tests/testLoyaltyProgram.spec.ts @@ -27,10 +27,17 @@ const partyData = { email: 'john@whoe.com', }; +const today = new Date(); +const fromDate = new Date(today); +fromDate.setDate(today.getDate() - 10); + +const toDate = new Date(today); +toDate.setDate(today.getDate() + 20); + const loyaltyProgramData = { name: 'program', - fromDate: new Date('12/10/2024'), - toDate: new Date('12/30/2024'), + fromDate: fromDate, + toDate: toDate, email: 'sample@gmail.com', mobile: '1234567890', expenseAccount: accountData.name, @@ -118,11 +125,11 @@ async function loyaltyPointEntryDoc(sinvName: string) { } } -async function createSalesInvoice() { +async function createSalesInvoice(invoiceDate?: Date) { const sinvDoc = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, { account: 'Debtors', party: partyData.name, - date: new Date('12/11/2024'), + date: invoiceDate || new Date(), items: [ { item: itemData.name, @@ -189,10 +196,10 @@ test('create Sales Invoice and verify loyalty points are created correctly', asy }); test('create SINV with future date and verify loyalty points are not created', async (t) => { - const futureDate = new Date(new Date().setDate(new Date().getDate() + 20)); + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); - const sinvDoc = await createSalesInvoice(); - sinvDoc.date = futureDate; + const sinvDoc = await createSalesInvoice(futureDate); await sinvDoc.sync(); await sinvDoc.submit(); diff --git a/models/helpers.ts b/models/helpers.ts index ac458d13..da4af4e4 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -743,6 +743,74 @@ export function getDocStatusListColumn(): ColumnConfig { }; } +export function getLoyaltyProgramStatusColumn(): ColumnConfig { + return { + label: t`Status`, + fieldname: 'status', + fieldtype: 'Select', + render(doc) { + const status = getLoyaltyProgramStatus(doc); + const color = loyaltyProgramStatusColor[status] ?? 'gray'; + const label = getLoyaltyProgramStatusText(status); + + return { + template: `${label}`, + metadata: { + status, + color, + label, + }, + }; + }, + }; +} + +export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { + if (!doc) { + return ''; + } + + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const toDate = doc.toDate as Date; + + if (toDate && toDate <= currentDate) { + return 'Expired'; + } + + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; + } + + return 'Active'; +} + +export const loyaltyProgramStatusColor: Record = { + Active: 'green', + Disabled: 'gray', + Expired: 'red', + Maxed: 'orange', +}; + +export function getLoyaltyProgramStatusText(status: string): string { + switch (status) { + case 'Active': + return t`Active`; + case 'Disabled': + return t`Disabled`; + case 'Expired': + return t`Expired`; + case 'Maxed': + return t`Maxed`; + default: + return ''; + } +} + type ModelsWithItems = Invoice | StockTransfer | StockMovement; export async function addItem(name: string, doc: M) { if (!doc.canEdit) { @@ -888,6 +956,15 @@ export async function createLoyaltyPointEntry(doc: Invoice) { if (!loyaltyProgramDoc.isEnabled) { return; } + + const toDate = loyaltyProgramDoc.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + return; + } + const expiryDate = new Date(Date.now()); expiryDate.setDate( @@ -956,18 +1033,22 @@ export function getLoyaltyProgramTier( let loyaltyProgramTier: CollectionRulesItems | undefined; for (const row of loyaltyProgramData.collectionRules) { - if (isPesa(row.minimumTotalSpent)) { - const minimumSpent = row.minimumTotalSpent; + if (row.minimumTotalSpent !== undefined && row.minimumTotalSpent !== null) { + let minimumSpent: Money; - if (!minimumSpent.lte(grandTotal)) { - continue; + if (isPesa(row.minimumTotalSpent)) { + minimumSpent = row.minimumTotalSpent; + } else { + minimumSpent = new Money(row.minimumTotalSpent as number); } - if ( - !loyaltyProgramTier || - minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money) - ) { - loyaltyProgramTier = row; + if (minimumSpent.lte(grandTotal)) { + if ( + !loyaltyProgramTier || + minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money) + ) { + loyaltyProgramTier = row; + } } } } @@ -1542,6 +1623,52 @@ export async function validateCouponCode( } } +export async function validateLoyaltyProgram( + doc: Invoice, + loyaltyProgramName: string +) { + const loyaltyProgram = await doc.fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['fromDate', 'toDate', 'maximumUse', 'used', 'isEnabled'], + filters: { name: loyaltyProgramName }, + }); + + if (!loyaltyProgram[0]?.isEnabled) { + throw new ValidationError( + 'Loyalty program cannot be applied as it is not enabled' + ); + } + + if ( + (loyaltyProgram[0]?.maximumUse as number) > 0 && + (loyaltyProgram[0]?.used as number) >= + (loyaltyProgram[0]?.maximumUse as number) + ) { + throw new ValidationError( + 'Loyalty program has reached maximum usage limit' + ); + } + + if ( + loyaltyProgram[0].fromDate && + (doc.date as Date) < (loyaltyProgram[0].fromDate as Date) + ) { + throw new ValidationError('Loyalty program is not yet active'); + } + + const toDate = loyaltyProgram[0].toDate as Date; + if (toDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const normalizedToDate = new Date(toDate); + normalizedToDate.setHours(0, 0, 0, 0); + + // Only throw error if toDate is clearly in the past + if (normalizedToDate.getTime() < today.getTime()) { + throw new ValidationError('Loyalty program has expired'); + } + } +} + export function removeFreeItems(sinvDoc: SalesInvoice) { if (!sinvDoc || !sinvDoc.items) { return; @@ -1616,3 +1743,36 @@ export function roundFreeItemQty( ): number { return Math[roundingMethod](quantity); } + +export async function isLoyaltyProgramExpiredAndMaxed( + fyo: Fyo, + loyaltyProgramName: string +): Promise { + if (!loyaltyProgramName) { + return false; + } + + const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['toDate', 'maximumUse', 'used', 'isEnabled'], + filters: { name: loyaltyProgramName }, + }); + + if (!loyaltyProgram.length) { + return false; + } + + const program = loyaltyProgram[0]; + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const toDate = program.toDate as Date; + const isExpired = + toDate && new Date(toDate).getTime() < currentDate.getTime(); + + const maximumUse = (program.maximumUse as number) || 0; + const used = (program.used as number) || 0; + const isMaxed = maximumUse > 0 && used >= maximumUse; + + const result = isExpired || isMaxed; + return result; +} diff --git a/schemas/app/LoyaltyProgram.json b/schemas/app/LoyaltyProgram.json index 5c5c1c71..608f97f9 100644 --- a/schemas/app/LoyaltyProgram.json +++ b/schemas/app/LoyaltyProgram.json @@ -51,7 +51,7 @@ "label": "Expiry Duration", "fieldtype": "Int", "default": 1, - "required": true + "hidden": true }, { "fieldname": "expenseAccount", @@ -59,6 +59,21 @@ "fieldtype": "Link", "target": "Account", "required": true + }, + { + "fieldname": "maximumUse", + "label": "Maximum Use", + "fieldtype": "Int", + "default": 0, + "section": "Validity and Usage" + }, + { + "fieldname": "used", + "label": "Used", + "fieldtype": "Int", + "default": 0, + "readOnly": true, + "section": "Validity and Usage" } ], "quickEditFields": [ @@ -67,7 +82,8 @@ "toDate", "conversionFactor", "expenseAccount", - "expiryDuration" + "maximumUse", + "used" ], "keywordFields": ["name"] } diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index f372a543..fcdb4645 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -82,7 +82,7 @@ "fieldtype": "Link", "target": "LoyaltyProgram", "label": "Loyalty Program", - "section": "References", + "section": "Loyalty Points Redemption", "readOnly": true }, { @@ -90,7 +90,7 @@ "fieldtype": "Int", "label": "Available Loyalty Points", "readOnly": true, - "section": "References" + "section": "Loyalty Points Redemption" }, { "fieldname": "redeemLoyaltyPoints", diff --git a/src/components/StatusPill.vue b/src/components/StatusPill.vue index f2d1743a..71baed7d 100644 --- a/src/components/StatusPill.vue +++ b/src/components/StatusPill.vue @@ -8,6 +8,7 @@ import { Doc } from 'fyo/model/doc'; import { isPesa } from 'fyo/utils'; import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { Party } from 'models/baseModels/Party/Party'; +import { LoyaltyProgram } from 'models/baseModels/LoyaltyProgram/LoyaltyProgram'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { getBgTextColorClass } from 'src/utils/colors'; @@ -77,6 +78,9 @@ export default defineComponent({ ReturnIssued: this.t`Return Issued`, Unpaid: this.t`Unpaid`, PartlyPaid: this.t`Partly Paid`, + Expired: this.t`Expired`, + Active: this.t`Active`, + Maxed: this.t`Maxed`, }[this.status]; }, color(): UIColors { @@ -99,6 +103,9 @@ const statusColorMap: Record = { ReturnIssued: 'gray', Unpaid: 'red', PartlyPaid: 'yellow', + Expired: 'red', + Active: 'green', + Maxed: 'orange', }; function getStatus(doc: Doc) { @@ -110,6 +117,27 @@ function getStatus(doc: Doc) { return 'NotSaved'; } + if (doc instanceof LoyaltyProgram) { + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; + } + + if (doc.toDate && doc.toDate instanceof Date) { + const toDate = new Date(doc.toDate); + toDate.setHours(0, 0, 0, 0); + if (toDate <= currentDate) { + return 'Expired'; + } + } + return 'Active'; + } + if (doc instanceof Party && doc.outstandingAmount?.isZero() !== true) { return 'Outstanding'; } diff --git a/src/pages/POS/LoyaltyProgramModal.vue b/src/pages/POS/LoyaltyProgramModal.vue index a4e921d9..d99f3cbc 100644 --- a/src/pages/POS/LoyaltyProgramModal.vue +++ b/src/pages/POS/LoyaltyProgramModal.vue @@ -128,6 +128,22 @@ export default defineComponent({ return; } + const loyaltyProgramDoc = await this.fyo.db.getAll( + ModelNameEnum.LoyaltyProgram, + { + fields: ['conversionFactor', 'toDate'], + filters: { name: partyData.loyaltyProgram as string }, + } + ); + + const toDate = loyaltyProgramDoc[0]?.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + throw new Error(t`Loyalty program has expired and cannot be applied`); + } + if (this.loyaltyPoints >= newValue) { this.sinvDoc.loyaltyPoints = newValue; } else { @@ -138,14 +154,6 @@ export default defineComponent({ ); } - const loyaltyProgramDoc = await this.fyo.db.getAll( - ModelNameEnum.LoyaltyProgram, - { - fields: ['conversionFactor'], - filters: { name: partyData.loyaltyProgram as string }, - } - ); - const loyaltyPoint = newValue * ((loyaltyProgramDoc[0]?.conversionFactor as number) || 0); diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index 154a85dd..81aaa8b1 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -168,6 +168,7 @@ import { removeFreeItems, getItemRateFromPriceList, getItemVisibility, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { ItemVisibility } from 'src/components/POS/types'; import { @@ -503,7 +504,21 @@ export default defineComponent({ filters: { name: value }, }); - this.loyaltyProgram = party[0]?.loyaltyProgram as string; + const loyaltyProgramName = party[0]?.loyaltyProgram as string; + + if (loyaltyProgramName) { + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( + this.fyo, + loyaltyProgramName + ); + if (isExpiredAndMaxed) { + this.loyaltyProgram = loyaltyProgramName; + this.loyaltyPoints = 0; + return; + } + } + + this.loyaltyProgram = loyaltyProgramName; this.loyaltyPoints = party[0]?.loyaltyPoints as number; }, @@ -652,7 +667,8 @@ export default defineComponent({ this.fyo.singles.AccountingSettings?.enablePriceList && this.loyaltyPoints && this.sinvDoc.party && - this.sinvDoc.items?.length + this.sinvDoc.items?.length && + this.loyaltyProgram ) { this.toggleModal('LoyaltyProgram', true); } diff --git a/src/pages/POS/POSQuickActions.vue b/src/pages/POS/POSQuickActions.vue index 51f118b7..54572f48 100644 --- a/src/pages/POS/POSQuickActions.vue +++ b/src/pages/POS/POSQuickActions.vue @@ -76,7 +76,9 @@