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: coupon codes #930

Merged
merged 20 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading