Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: loyalty program #933

Merged
merged 13 commits into from
Sep 11, 2024
4 changes: 4 additions & 0 deletions models/baseModels/AccountingSettings/AccountingSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class AccountingSettings extends Doc {
enableLead?: boolean;
enableFormCustomization?: boolean;
enableInvoiceReturns?: boolean;
enableLoyaltyProgram?: boolean;
enablePricingRule?: boolean;

static filters: FiltersMap = {
Expand Down Expand Up @@ -56,6 +57,9 @@ export class AccountingSettings extends Doc {
enableInvoiceReturns: () => {
return !!this.enableInvoiceReturns;
},
enableLoyaltyProgram: () => {
return !!this.enableLoyaltyProgram;
},
};

override hidden: HiddenMap = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Doc } from 'fyo/model/doc';
import { Money } from 'pesa';

export class CollectionRulesItems extends Doc {
tierName?: string;
collectionFactor?: number;
minimumTotalSpent?: Money;
}
85 changes: 81 additions & 4 deletions models/baseModels/Invoice/Invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import { Transactional } from 'models/Transactional/Transactional';
import {
addItem,
canApplyPricingRule,
createLoyaltyPointEntry,
filterPricingRules,
getAddedLPWithGrandTotal,
getExchangeRate,
getNumberSeries,
getPricingRulesConflicts,
removeLoyaltyPoint,
roundFreeItemQty,
} from 'models/helpers';
import { StockTransfer } from 'models/inventory/StockTransfer';
Expand All @@ -38,6 +41,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;
Expand Down Expand Up @@ -69,8 +73,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;
Expand Down Expand Up @@ -179,17 +185,31 @@ export abstract class Invoice extends Transactional {
async afterSubmit() {
await super.afterSubmit();

if (this.isReturn) {
await this._removeLoyaltyPointEntry();
}

if (this.isQuote) {
return;
}

let lpAddedBaseGrandTotal: Money | undefined;

if (this.redeemLoyaltyPoints) {
lpAddedBaseGrandTotal = await this.getLPAddedBaseGrandTotal();
}

// update outstanding amounts
await this.fyo.db.update(this.schemaName, {
name: this.name as string,
outstandingAmount: this.baseGrandTotal!,
outstandingAmount: lpAddedBaseGrandTotal! || this.baseGrandTotal!,
});

const party = (await this.fyo.doc.getDoc('Party', this.party)) as Party;
const party = (await this.fyo.doc.getDoc(
ModelNameEnum.Party,
this.party
)) as Party;

await party.updateOutstandingAmount();

if (this.makeAutoPayment && this.autoPaymentAccount) {
Expand All @@ -207,13 +227,22 @@ export abstract class Invoice extends Transactional {
}

await this._updateIsItemsReturned();
await this._createLoyaltyPointEntry();
}

async afterCancel() {
await super.afterCancel();
await this._cancelPayments();
await this._updatePartyOutStanding();
await this._updateIsItemsReturned();
await this._removeLoyaltyPointEntry();
}

async _removeLoyaltyPointEntry() {
if (!this.loyaltyProgram) {
return;
}
await removeLoyaltyPoint(this);
}

async _cancelPayments() {
Expand Down Expand Up @@ -538,6 +567,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;
Expand All @@ -560,6 +611,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 () => {
Expand All @@ -571,6 +632,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(
Expand Down Expand Up @@ -611,12 +682,15 @@ export abstract class Invoice extends Transactional {
dependsOn: ['grandTotal', 'exchangeRate'],
},
outstandingAmount: {
formula: () => {
formula: async () => {
if (this.submitted) {
return;
}
if (this.redeemLoyaltyPoints) {
return await this.getLPAddedBaseGrandTotal();
}

return this.baseGrandTotal!;
return this.baseGrandTotal;
},
},
stockNotTransferred: {
Expand Down Expand Up @@ -726,6 +800,9 @@ export abstract class Invoice extends Transactional {
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
backReference: () => !this.backReference,
quote: () => !this.quote,
loyaltyProgram: () => !this.loyaltyProgram,
loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn,
redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
priceList: () =>
!this.fyo.singles.AccountingSettings?.enablePriceList ||
(!this.canEdit && !this.priceList),
Expand Down
21 changes: 21 additions & 0 deletions models/baseModels/LoyaltyPointEntry/LoyaltyPointEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';

export class LoyaltyPointEntry extends Doc {
loyaltyProgram?: string;
customer?: string;
invoice?: string;
purchaseAmount?: number;
expiryDate?: Date;

static override getListViewSettings(): ListViewSettings {
return {
columns: [
'loyaltyProgram',
'customer',
'purchaseAmount',
'loyaltyPoints',
],
};
}
}
22 changes: 22 additions & 0 deletions models/baseModels/LoyaltyProgram/LoyaltyProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Doc } from 'fyo/model/doc';
import { FiltersMap, ListViewSettings } from 'fyo/model/types';
import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems';
import { AccountRootTypeEnum } from '../Account/types';

export class LoyaltyProgram extends Doc {
collectionRules?: CollectionRulesItems[];
expiryDuration?: number;

static filters: FiltersMap = {
expenseAccount: () => ({
rootType: AccountRootTypeEnum.Liability,
isGroup: false,
}),
};

static getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'fromDate', 'toDate', 'expiryDuration'],
};
}
}
35 changes: 35 additions & 0 deletions models/baseModels/Party/Party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class Party extends Doc {
party?: string;
fromLead?: string;
defaultAccount?: string;
loyaltyPoints?: number;
outstandingAmount?: Money;
async updateOutstandingAmount() {
/**
Expand Down Expand Up @@ -54,6 +55,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'
) {
Expand Down
Loading
Loading