Skip to content

Commit

Permalink
Merge pull request #930 from AbleKSaju/feat-couponCode
Browse files Browse the repository at this point in the history
feat: coupon codes
  • Loading branch information
akshayitzme authored Oct 2, 2024
2 parents 73b58d8 + 872f967 commit 5b564c2
Show file tree
Hide file tree
Showing 18 changed files with 1,009 additions and 21 deletions.
3 changes: 3 additions & 0 deletions models/baseModels/AccountingSettings/AccountingSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class AccountingSettings extends Doc {
enableInventory?: boolean;
enablePriceList?: boolean;
enableLead?: boolean;
enableCouponCode?: boolean;
enableFormCustomization?: boolean;
enableInvoiceReturns?: boolean;
enableLoyaltyProgram?: boolean;
Expand Down Expand Up @@ -67,6 +68,8 @@ export class AccountingSettings extends Doc {
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
enablePricingRule: () =>
!this.fyo.singles.AccountingSettings?.enableDiscounting,
enableCouponCode: () =>
!this.fyo.singles.AccountingSettings?.enablePricingRule,
};

async change(ch: ChangeArg) {
Expand Down
107 changes: 107 additions & 0 deletions models/baseModels/AppliedCouponCodes/AppliedCouponCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { DocValue } from 'fyo/core/types';
import { ValidationMap } from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
import { getApplicableCouponCodesName } from 'models/helpers';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';

export class AppliedCouponCodes extends InvoiceItem {
coupons?: string;

validations: ValidationMap = {
coupons: async (value: DocValue) => {
if (!value) {
return;
}

const coupon = await this.fyo.db.getAll(ModelNameEnum.CouponCode, {
fields: [
'minAmount',
'maxAmount',
'pricingRule',
'validFrom',
'validTo',
'maximumUse',
'used',
'isEnabled',
],
filters: { name: value as string },
});

if (!coupon[0].isEnabled) {
throw new ValidationError(
'Coupon code cannot be applied as it is not enabled'
);
}

if ((coupon[0]?.maximumUse as number) <= (coupon[0]?.used as number)) {
throw new ValidationError(
'Coupon code has been used maximum number of times'
);
}

const applicableCouponCodesNames = await getApplicableCouponCodesName(
value as string,
this.parentdoc as SalesInvoice
);

if (!applicableCouponCodesNames?.length) {
throw new ValidationError(
this.fyo.t`Coupon ${
value as string
} is not applicable for applied items.`
);
}

const couponExist = this.parentdoc?.coupons?.some(
(coupon) => coupon?.coupons === value
);

if (couponExist) {
throw new ValidationError(
this.fyo.t`${value as string} already applied.`
);
}

if (
(coupon[0].minAmount as Money).gte(
this.parentdoc?.grandTotal as Money
) &&
!(coupon[0].minAmount as Money).isZero()
) {
throw new ValidationError(
this.fyo.t`The Grand Total must exceed ${
(coupon[0].minAmount as Money).float
} to apply the coupon ${value as string}.`
);
}

if (
(coupon[0].maxAmount as Money).lte(
this.parentdoc?.grandTotal as Money
) &&
!(coupon[0].maxAmount as Money).isZero()
) {
throw new ValidationError(
this.fyo.t`The Grand Total must be less than ${
(coupon[0].maxAmount as Money).float
} to apply this coupon.`
);
}

if ((coupon[0].validFrom as Date) > (this.parentdoc?.date as Date)) {
throw new ValidationError(
this.fyo.t`Valid From Date should be less than Valid To Date.`
);
}

if ((coupon[0].validTo as Date) < (this.parentdoc?.date as Date)) {
throw new ValidationError(
this.fyo.t`Valid To Date should be greater than Valid From Date.`
);
}
},
};
}
192 changes: 192 additions & 0 deletions models/baseModels/CouponCode/CouponCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import {
FiltersMap,
FormulaMap,
ListViewSettings,
ValidationMap,
} from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { t } from 'fyo';
import { Money } from 'pesa';
import { ModelNameEnum } from 'models/types';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
import { ApplicableCouponCodes } from '../Invoice/types';

export class CouponCode extends Doc {
name?: string;
couponName?: string;
pricingRule?: string;

validFrom?: Date;
validTo?: Date;

minAmount?: Money;
maxAmount?: Money;

removeUnusedCoupons(coupons: ApplicableCouponCodes[], sinvDoc: SalesInvoice) {
if (!coupons.length) {
sinvDoc.coupons = [];

return;
}

sinvDoc.coupons = sinvDoc.coupons!.filter((coupon) => {
return coupons.find((c: ApplicableCouponCodes) =>
coupon?.coupons?.includes(c?.coupon)
);
});
}

formulas: FormulaMap = {
name: {
formula: () => {
return this.couponName?.replace(/\s+/g, '').toUpperCase().slice(0, 8);
},
dependsOn: ['couponName'],
},
};

async pricingRuleData() {
return await this.fyo.db.getAll(ModelNameEnum.PricingRule, {
fields: ['minAmount', 'maxAmount', 'validFrom', 'validTo'],
filters: {
name: this.pricingRule as string,
},
});
}

validations: ValidationMap = {
minAmount: async (value: DocValue) => {
if (!value || !this.maxAmount || !this.pricingRule) {
return;
}

const [pricingRuleData] = await this.pricingRuleData();

if (
(pricingRuleData?.minAmount as Money).isZero() &&
(pricingRuleData.maxAmount as Money).isZero()
) {
return;
}

const { minAmount } = pricingRuleData;

if ((value as Money).isZero() && this.maxAmount.isZero()) {
return;
}

if ((value as Money).lt(minAmount as Money)) {
throw new ValidationError(
t`Minimum Amount should be greather than the Pricing Rule's Minimum Amount.`
);
}

if ((value as Money).gte(this.maxAmount)) {
throw new ValidationError(
t`Minimum Amount should be less than the Maximum Amount.`
);
}
},
maxAmount: async (value: DocValue) => {
if (!this.minAmount || !value || !this.pricingRule) {
return;
}

const [pricingRuleData] = await this.pricingRuleData();

if (
(pricingRuleData?.minAmount as Money).isZero() &&
(pricingRuleData.maxAmount as Money).isZero()
) {
return;
}

const { maxAmount } = pricingRuleData;

if (this.minAmount.isZero() && (value as Money).isZero()) {
return;
}

if ((value as Money).gt(maxAmount as Money)) {
throw new ValidationError(
t`Maximum Amount should be lesser than Pricing Rule's Maximum Amount`
);
}

if ((value as Money).lte(this.minAmount)) {
throw new ValidationError(
t`Maximum Amount should be greater than the Minimum Amount.`
);
}
},
validFrom: async (value: DocValue) => {
if (!value || !this.validTo || !this.pricingRule) {
return;
}

const [pricingRuleData] = await this.pricingRuleData();

if (!pricingRuleData?.validFrom && !pricingRuleData.validTo) {
return;
}

const { validFrom } = pricingRuleData;
if (
validFrom &&
(value as Date).toISOString() < (validFrom as Date).toISOString()
) {
throw new ValidationError(
t`Valid From Date should be greather than Pricing Rule's Valid From Date.`
);
}

if ((value as Date).toISOString() >= this.validTo.toISOString()) {
throw new ValidationError(
t`Valid From Date should be less than Valid To Date.`
);
}
},
validTo: async (value: DocValue) => {
if (!this.validFrom || !value || !this.pricingRule) {
return;
}

const [pricingRuleData] = await this.pricingRuleData();

if (!pricingRuleData?.validFrom && !pricingRuleData.validTo) {
return;
}

const { validTo } = pricingRuleData;

if (
validTo &&
(value as Date).toISOString() > (validTo as Date).toISOString()
) {
throw new ValidationError(
t`Valid To Date should be lesser than Pricing Rule's Valid To Date.`
);
}

if ((value as Date).toISOString() <= this.validFrom.toISOString()) {
throw new ValidationError(
t`Valid To Date should be greater than Valid From Date.`
);
}
},
};

static filters: FiltersMap = {
pricingRule: () => ({
isCouponCodeBased: true,
}),
};

static getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'couponName', 'pricingRule', 'maximumUse', 'used'],
};
}
}
Loading

0 comments on commit 5b564c2

Please sign in to comment.