-
Notifications
You must be signed in to change notification settings - Fork 688
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #930 from AbleKSaju/feat-couponCode
feat: coupon codes
- Loading branch information
Showing
18 changed files
with
1,009 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
models/baseModels/AppliedCouponCodes/AppliedCouponCodes.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.` | ||
); | ||
} | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'], | ||
}; | ||
} | ||
} |
Oops, something went wrong.