diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts index e6e6b2ab..ab5eb70c 100644 --- a/jobs/checkLoyaltyProgramExpiry.ts +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -10,26 +10,13 @@ export async function checkLoyaltyProgramExpiry() { const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'], filters: { - status: ['not in', ['Expired', 'Maxed']], + status: ['not in', ['Expired']], isEnabled: true, }, }); if (loyaltyPrograms) { for (const program of loyaltyPrograms) { - const maximumUse = Number(program.maximumUse) || 0; - const used = Number(program.used) || 0; - - if (maximumUse > 0 && used >= maximumUse) { - await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) - .where({ name: program.name }) - .update({ - status: 'Maxed', - isEnabled: false, - }); - continue; - } - if (program.toDate && new Date(String(program.toDate)) <= currentDate) { await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) .where({ name: program.name }) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 655c6230..049470af 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -30,7 +30,7 @@ import { getItemVisibility, validateLoyaltyProgram, getLoyaltyProgramTier, - isLoyaltyProgramMaxedOut, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -886,7 +886,7 @@ export abstract class Invoice extends Transactional { } async updateUsedCountOfLoyaltyProgram() { - if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { + if (!this.loyaltyProgram) { return; } @@ -895,13 +895,27 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram ); - await loyaltyProgramDoc.setAndSync({ - used: (loyaltyProgramDoc.used as number) + 1, - }); + 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 || !this.redeemLoyaltyPoints) { + if (!this.loyaltyProgram) { return; } @@ -910,9 +924,22 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram ); - await loyaltyProgramDoc.setAndSync({ - used: (loyaltyProgramDoc.used as number) - 1, - }); + 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) { @@ -975,11 +1002,6 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram )) as LoyaltyProgram; - // Check if loyalty program is enabled - if (!loyaltyProgramDoc.isEnabled) { - return; - } - const invoiceDate = this.date as Date; const fromDate = loyaltyProgramDoc.fromDate as Date; const toDate = loyaltyProgramDoc.toDate as Date; @@ -1093,12 +1115,7 @@ export abstract class Invoice extends Transactional { return ''; } - const maxedOut = await isLoyaltyProgramMaxedOut( - this.fyo, - loyaltyProgramName - ); - - return maxedOut ? '' : loyaltyProgramName; + return loyaltyProgramName; }, dependsOn: ['party', 'name'], }, @@ -1108,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, @@ -1115,7 +1143,7 @@ export abstract class Invoice extends Transactional { ); return loyaltyPoints || 0; }, - dependsOn: ['party'], + dependsOn: ['party', 'loyaltyProgram'], }, currency: { formula: async () => { @@ -1308,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 d2bb47bd..20d8db45 100644 --- a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts +++ b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts @@ -1,9 +1,8 @@ import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FiltersMap, ListViewSettings, ValidationMap } 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 { @@ -11,7 +10,7 @@ export class LoyaltyProgram extends Doc { expiryDuration?: number; maximumUse?: number; used?: number; - status?: 'Active' | 'Expired' | 'Maxed' | 'Disabled'; + status?: 'Active' | 'Expired' | 'Disabled' | 'Maxed'; validations: ValidationMap = { used: (value: DocValue) => { @@ -35,22 +34,6 @@ export class LoyaltyProgram extends Doc { }, }; - async afterSubmit() { - const maximumUse = (this.maximumUse as number) || 0; - const used = (this.used as number) || 0; - - if (maximumUse > 0 && used >= maximumUse) { - await this.setAndSync({ status: 'Maxed', isEnabled: false }); - } - } - - static filters: FiltersMap = { - expenseAccount: () => ({ - rootType: AccountRootTypeEnum.Expense, - isGroup: false, - }), - }; - static getListViewSettings(): ListViewSettings { return { 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/helpers.ts b/models/helpers.ts index f0a21bc8..467940c2 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -765,13 +765,6 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { return ''; } - const maximumUse = doc.maximumUse as number; - const used = doc.used as number; - - if (maximumUse > 0 && used >= maximumUse) { - return 'Maxed'; - } - const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); @@ -781,6 +774,13 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { return 'Expired'; } + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; + } + return 'Active'; } @@ -1664,33 +1664,6 @@ export async function validateLoyaltyProgram( } } -export async function isLoyaltyProgramMaxedOut( - fyo: Fyo, - loyaltyProgramName: string -): Promise { - const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { - fields: ['maximumUse', 'used', 'isEnabled'], - filters: { name: loyaltyProgramName }, - }); - - if (!loyaltyProgram[0]) { - return false; - } - - if (!loyaltyProgram[0]?.isEnabled) { - return true; - } - - const maximumUse = loyaltyProgram[0]?.maximumUse as number; - const used = loyaltyProgram[0]?.used as number; - - if (!maximumUse) { - return false; - } - - return used >= maximumUse; -} - export function removeFreeItems(sinvDoc: SalesInvoice) { if (!sinvDoc || !sinvDoc.items) { return; @@ -1765,3 +1738,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/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 358d9f4e..71baed7d 100644 --- a/src/components/StatusPill.vue +++ b/src/components/StatusPill.vue @@ -79,8 +79,8 @@ export default defineComponent({ Unpaid: this.t`Unpaid`, PartlyPaid: this.t`Partly Paid`, Expired: this.t`Expired`, - Maxed: this.t`Maxed`, Active: this.t`Active`, + Maxed: this.t`Maxed`, }[this.status]; }, color(): UIColors { @@ -104,8 +104,8 @@ const statusColorMap: Record = { Unpaid: 'red', PartlyPaid: 'yellow', Expired: 'red', - Maxed: 'orange', Active: 'green', + Maxed: 'orange', }; function getStatus(doc: Doc) { diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index 2bb2c11d..d854be35 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -166,7 +166,7 @@ import { removeFreeItems, getItemRateFromPriceList, getItemVisibility, - isLoyaltyProgramMaxedOut, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { POSItem, @@ -499,12 +499,12 @@ export default defineComponent({ const loyaltyProgramName = party[0]?.loyaltyProgram as string; if (loyaltyProgramName) { - const isMaxedOut = await isLoyaltyProgramMaxedOut( + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( this.fyo, loyaltyProgramName ); - if (isMaxedOut) { - this.loyaltyProgram = ''; + if (isExpiredAndMaxed) { + this.loyaltyProgram = loyaltyProgramName; this.loyaltyPoints = 0; return; }