fix: added validation for maxed loyaltyprogram

This commit is contained in:
Gadha2311
2026-02-04 15:07:22 +05:30
parent 8e10bf313d
commit cb092ede86
8 changed files with 119 additions and 97 deletions
+1 -14
View File
@@ -10,26 +10,13 @@ export async function checkLoyaltyProgramExpiry() {
const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, {
fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'], fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'],
filters: { filters: {
status: ['not in', ['Expired', 'Maxed']], status: ['not in', ['Expired']],
isEnabled: true, isEnabled: true,
}, },
}); });
if (loyaltyPrograms) { if (loyaltyPrograms) {
for (const program of 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) { if (program.toDate && new Date(String(program.toDate)) <= currentDate) {
await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) await dm.db?.knex!(ModelNameEnum.LoyaltyProgram)
.where({ name: program.name }) .where({ name: program.name })
+56 -22
View File
@@ -30,7 +30,7 @@ import {
getItemVisibility, getItemVisibility,
validateLoyaltyProgram, validateLoyaltyProgram,
getLoyaltyProgramTier, getLoyaltyProgramTier,
isLoyaltyProgramMaxedOut, isLoyaltyProgramExpiredAndMaxed,
} from 'models/helpers'; } from 'models/helpers';
import { StockTransfer } from 'models/inventory/StockTransfer'; import { StockTransfer } from 'models/inventory/StockTransfer';
import { validateBatch } from 'models/inventory/helpers'; import { validateBatch } from 'models/inventory/helpers';
@@ -886,7 +886,7 @@ export abstract class Invoice extends Transactional {
} }
async updateUsedCountOfLoyaltyProgram() { async updateUsedCountOfLoyaltyProgram() {
if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { if (!this.loyaltyProgram) {
return; return;
} }
@@ -895,13 +895,27 @@ export abstract class Invoice extends Transactional {
this.loyaltyProgram this.loyaltyProgram
); );
await loyaltyProgramDoc.setAndSync({ const maximumUse = loyaltyProgramDoc.maximumUse as number;
used: (loyaltyProgramDoc.used as number) + 1, 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() { async reduceUsedCountOfLoyaltyProgram() {
if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { if (!this.loyaltyProgram) {
return; return;
} }
@@ -910,9 +924,22 @@ export abstract class Invoice extends Transactional {
this.loyaltyProgram this.loyaltyProgram
); );
await loyaltyProgramDoc.setAndSync({ const maximumUse = loyaltyProgramDoc.maximumUse as number;
used: (loyaltyProgramDoc.used as number) - 1, 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) { async updateIsItemsFullyReturned(doc?: Invoice) {
@@ -975,11 +1002,6 @@ export abstract class Invoice extends Transactional {
this.loyaltyProgram this.loyaltyProgram
)) as LoyaltyProgram; )) as LoyaltyProgram;
// Check if loyalty program is enabled
if (!loyaltyProgramDoc.isEnabled) {
return;
}
const invoiceDate = this.date as Date; const invoiceDate = this.date as Date;
const fromDate = loyaltyProgramDoc.fromDate as Date; const fromDate = loyaltyProgramDoc.fromDate as Date;
const toDate = loyaltyProgramDoc.toDate as Date; const toDate = loyaltyProgramDoc.toDate as Date;
@@ -1093,12 +1115,7 @@ export abstract class Invoice extends Transactional {
return ''; return '';
} }
const maxedOut = await isLoyaltyProgramMaxedOut( return loyaltyProgramName;
this.fyo,
loyaltyProgramName
);
return maxedOut ? '' : loyaltyProgramName;
}, },
dependsOn: ['party', 'name'], dependsOn: ['party', 'name'],
}, },
@@ -1108,6 +1125,17 @@ export abstract class Invoice extends Transactional {
return 0; 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( const loyaltyPoints = await this.fyo.getValue(
ModelNameEnum.Party, ModelNameEnum.Party,
this.party, this.party,
@@ -1115,7 +1143,7 @@ export abstract class Invoice extends Transactional {
); );
return loyaltyPoints || 0; return loyaltyPoints || 0;
}, },
dependsOn: ['party'], dependsOn: ['party', 'loyaltyProgram'],
}, },
currency: { currency: {
formula: async () => { formula: async () => {
@@ -1308,7 +1336,13 @@ export abstract class Invoice extends Transactional {
loyaltyProgram: () => !this.loyaltyProgram, loyaltyProgram: () => !this.loyaltyProgram,
availableLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn, availableLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
loyaltyPoints: () => !this.redeemLoyaltyPoints || 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, coupons: () => this.isSubmitted && !this.coupons?.length,
priceList: () => priceList: () =>
!this.fyo.singles.AccountingSettings?.enablePriceList || !this.fyo.singles.AccountingSettings?.enablePriceList ||
@@ -1,9 +1,8 @@
import { DocValue } from 'fyo/core/types'; import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; 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 { ValidationError } from 'fyo/utils/errors';
import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems'; import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems';
import { AccountRootTypeEnum } from '../Account/types';
import { getLoyaltyProgramStatusColumn } from '../../helpers'; import { getLoyaltyProgramStatusColumn } from '../../helpers';
export class LoyaltyProgram extends Doc { export class LoyaltyProgram extends Doc {
@@ -11,7 +10,7 @@ export class LoyaltyProgram extends Doc {
expiryDuration?: number; expiryDuration?: number;
maximumUse?: number; maximumUse?: number;
used?: number; used?: number;
status?: 'Active' | 'Expired' | 'Maxed' | 'Disabled'; status?: 'Active' | 'Expired' | 'Disabled' | 'Maxed';
validations: ValidationMap = { validations: ValidationMap = {
used: (value: DocValue) => { 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 { static getListViewSettings(): ListViewSettings {
return { return {
columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'], columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'],
+12
View File
@@ -14,6 +14,7 @@ import {
import { Money } from 'pesa'; import { Money } from 'pesa';
import { PartyRole } from './types'; import { PartyRole } from './types';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { isLoyaltyProgramExpiredAndMaxed } from 'models/helpers';
export class Party extends Doc { export class Party extends Doc {
role?: PartyRole; role?: PartyRole;
@@ -66,6 +67,17 @@ export class Party extends Doc {
} }
async _getTotalLoyaltyPoints() { 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, { const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, {
fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'], fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'],
filters: { filters: {
+40 -34
View File
@@ -765,13 +765,6 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string {
return ''; return '';
} }
const maximumUse = doc.maximumUse as number;
const used = doc.used as number;
if (maximumUse > 0 && used >= maximumUse) {
return 'Maxed';
}
const currentDate = new Date(); const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0); currentDate.setHours(0, 0, 0, 0);
@@ -781,6 +774,13 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string {
return 'Expired'; return 'Expired';
} }
const maximumUse = doc.maximumUse as number;
const used = doc.used as number;
if (maximumUse > 0 && used >= maximumUse) {
return 'Maxed';
}
return 'Active'; return 'Active';
} }
@@ -1664,33 +1664,6 @@ export async function validateLoyaltyProgram(
} }
} }
export async function isLoyaltyProgramMaxedOut(
fyo: Fyo,
loyaltyProgramName: string
): Promise<boolean> {
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) { export function removeFreeItems(sinvDoc: SalesInvoice) {
if (!sinvDoc || !sinvDoc.items) { if (!sinvDoc || !sinvDoc.items) {
return; return;
@@ -1765,3 +1738,36 @@ export function roundFreeItemQty(
): number { ): number {
return Math[roundingMethod](quantity); return Math[roundingMethod](quantity);
} }
export async function isLoyaltyProgramExpiredAndMaxed(
fyo: Fyo,
loyaltyProgramName: string
): Promise<boolean> {
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;
}
+2 -2
View File
@@ -82,7 +82,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"target": "LoyaltyProgram", "target": "LoyaltyProgram",
"label": "Loyalty Program", "label": "Loyalty Program",
"section": "References", "section": "Loyalty Points Redemption",
"readOnly": true "readOnly": true
}, },
{ {
@@ -90,7 +90,7 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Available Loyalty Points", "label": "Available Loyalty Points",
"readOnly": true, "readOnly": true,
"section": "References" "section": "Loyalty Points Redemption"
}, },
{ {
"fieldname": "redeemLoyaltyPoints", "fieldname": "redeemLoyaltyPoints",
+2 -2
View File
@@ -79,8 +79,8 @@ export default defineComponent({
Unpaid: this.t`Unpaid`, Unpaid: this.t`Unpaid`,
PartlyPaid: this.t`Partly Paid`, PartlyPaid: this.t`Partly Paid`,
Expired: this.t`Expired`, Expired: this.t`Expired`,
Maxed: this.t`Maxed`,
Active: this.t`Active`, Active: this.t`Active`,
Maxed: this.t`Maxed`,
}[this.status]; }[this.status];
}, },
color(): UIColors { color(): UIColors {
@@ -104,8 +104,8 @@ const statusColorMap: Record<Status, UIColors> = {
Unpaid: 'red', Unpaid: 'red',
PartlyPaid: 'yellow', PartlyPaid: 'yellow',
Expired: 'red', Expired: 'red',
Maxed: 'orange',
Active: 'green', Active: 'green',
Maxed: 'orange',
}; };
function getStatus(doc: Doc) { function getStatus(doc: Doc) {
+4 -4
View File
@@ -166,7 +166,7 @@ import {
removeFreeItems, removeFreeItems,
getItemRateFromPriceList, getItemRateFromPriceList,
getItemVisibility, getItemVisibility,
isLoyaltyProgramMaxedOut, isLoyaltyProgramExpiredAndMaxed,
} from 'models/helpers'; } from 'models/helpers';
import { import {
POSItem, POSItem,
@@ -499,12 +499,12 @@ export default defineComponent({
const loyaltyProgramName = party[0]?.loyaltyProgram as string; const loyaltyProgramName = party[0]?.loyaltyProgram as string;
if (loyaltyProgramName) { if (loyaltyProgramName) {
const isMaxedOut = await isLoyaltyProgramMaxedOut( const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed(
this.fyo, this.fyo,
loyaltyProgramName loyaltyProgramName
); );
if (isMaxedOut) { if (isExpiredAndMaxed) {
this.loyaltyProgram = ''; this.loyaltyProgram = loyaltyProgramName;
this.loyaltyPoints = 0; this.loyaltyPoints = 0;
return; return;
} }