@xyo-network/payment-plugin 3.0.18 → 3.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/neutral/Discount/Diviner.d.ts +3 -1
- package/dist/neutral/Discount/Diviner.d.ts.map +1 -1
- package/dist/neutral/Discount/lib/applyCoupons.d.ts.map +1 -1
- package/dist/neutral/index.mjs +59 -25
- package/dist/neutral/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/Discount/Diviner.ts +58 -37
- package/src/Discount/lib/applyCoupons.ts +42 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Address } from '@xylabs/hex';
|
|
1
|
+
import { Address, Hash } from '@xylabs/hex';
|
|
2
2
|
import { ArchivistInstance } from '@xyo-network/archivist-model';
|
|
3
3
|
import { AbstractDiviner } from '@xyo-network/diviner-abstract';
|
|
4
4
|
import { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease';
|
|
@@ -14,6 +14,8 @@ export declare class PaymentDiscountDiviner<TParams extends PaymentDiscountDivin
|
|
|
14
14
|
protected filterToSigned(coupons: Coupon[]): Promise<Coupon[]>;
|
|
15
15
|
protected getDiscountsArchivist(): Promise<ArchivistInstance>;
|
|
16
16
|
protected getDiscountsBoundWitnessDiviner(): Promise<DivinerInstance>;
|
|
17
|
+
protected getEscrowAppraisals(terms: EscrowTerms, hashMap: Record<Hash, Payload>): HashLeaseEstimate[];
|
|
18
|
+
protected getEscrowDiscounts(terms: EscrowTerms, hashMap: Record<Hash, Payload>): Promise<Coupon[]>;
|
|
17
19
|
protected isCouponCurrent(coupon: Coupon): boolean;
|
|
18
20
|
}
|
|
19
21
|
//# sourceMappingURL=Diviner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Diviner.d.ts","sourceRoot":"","sources":["../../../src/Discount/Diviner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"Diviner.d.ts","sourceRoot":"","sources":["../../../src/Discount/Diviner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAuB,MAAM,8BAA8B,CAAA;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAE/D,OAAO,EACL,iBAAiB,EAElB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EACc,eAAe,EAAE,sBAAsB,EAC3D,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACpD,OAAO,EACL,MAAM,EACN,QAAQ,EACR,WAAW,EAEqD,4BAA4B,EAC7F,MAAM,sCAAsC,CAAA;AAU7C,MAAM,MAAM,+BAA+B,GAAG,WAAW,GAAG,MAAM,GAAG,iBAAiB,GAAG,OAAO,CAAA;AAEhG,qBACa,sBAAsB,CACjC,OAAO,SAAS,4BAA4B,GAAG,4BAA4B,EAC3E,GAAG,SAAS,+BAA+B,GAAG,+BAA+B,EAC7E,IAAI,SAAS,QAAQ,GAAG,QAAQ,EAChC,UAAU,SAAS,sBAAsB,CAAC,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,sBAAsB,CAChH,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EACnC,GAAG,EACH,IAAI,CACL,CACD,SAAQ,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC;IACvD,OAAgB,aAAa,WAAuC;IACpE,OAAgB,mBAAmB,SAAqC;IAExE,SAAS,KAAK,iBAAiB,IAAI,OAAO,EAAE,CAE3C;cAEe,aAAa,CAAC,QAAQ,GAAE,GAAG,EAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;cAyCpD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;cAqBpD,qBAAqB,IAAI,OAAO,CAAC,iBAAiB,CAAC;cAMnD,+BAA+B,IAAI,OAAO,CAAC,eAAe,CAAC;IAY3E,SAAS,CAAC,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,iBAAiB,EAAE;cAYtF,kBAAkB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA2BzG,SAAS,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;CAInD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applyCoupons.d.ts","sourceRoot":"","sources":["../../../../src/Discount/lib/applyCoupons.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACxE,OAAO,KAAK,EAEV,MAAM,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"applyCoupons.d.ts","sourceRoot":"","sources":["../../../../src/Discount/lib/applyCoupons.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACxE,OAAO,KAAK,EAEV,MAAM,EAAE,QAAQ,EAGjB,MAAM,sCAAsC,CAAA;AAO7C,eAAO,MAAM,YAAY,eAAgB,iBAAiB,EAAE,WAAW,MAAM,EAAE,KAAG,QAiDjF,CAAA"}
|
package/dist/neutral/index.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
DiscountSchema,
|
|
41
41
|
isFixedAmountCoupon,
|
|
42
42
|
isFixedPercentageCoupon,
|
|
43
|
+
isFixedPriceCoupon,
|
|
43
44
|
isStackable
|
|
44
45
|
} from "@xyo-network/payment-payload-plugins";
|
|
45
46
|
var applyCoupons = (appraisals, coupons) => {
|
|
@@ -50,11 +51,14 @@ var applyCoupons = (appraisals, coupons) => {
|
|
|
50
51
|
const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0);
|
|
51
52
|
const singularFixedDiscount = Math.max(...coupons.filter((coupon) => isFixedAmountCoupon(coupon) && !isStackable(coupon)).map((coupon) => coupon.amount), 0);
|
|
52
53
|
const singularPercentageDiscount = Math.max(...coupons.filter((coupon) => isFixedPercentageCoupon(coupon) && !isStackable(coupon)).map((coupon) => coupon.percentage), 0) * total;
|
|
54
|
+
const singularFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, false);
|
|
53
55
|
const stackedFixedDiscount = coupons.filter((coupon) => isFixedAmountCoupon(coupon) && isStackable(coupon)).reduce((acc, coupon) => acc + coupon.amount, 0);
|
|
54
56
|
const stackedPercentageDiscount = coupons.filter((coupon) => isFixedPercentageCoupon(coupon) && isStackable(coupon)).reduce((acc, coupon) => acc + coupon.percentage, 0) * (total - stackedFixedDiscount);
|
|
55
|
-
const
|
|
57
|
+
const stackedFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, true);
|
|
58
|
+
const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount + stackedFixedPriceDiscount;
|
|
56
59
|
const maxDiscount = Math.max(
|
|
57
60
|
singularFixedDiscount,
|
|
61
|
+
singularFixedPriceDiscount,
|
|
58
62
|
singularPercentageDiscount,
|
|
59
63
|
stackedDiscount,
|
|
60
64
|
0
|
|
@@ -66,6 +70,18 @@ var applyCoupons = (appraisals, coupons) => {
|
|
|
66
70
|
currency: "USD"
|
|
67
71
|
};
|
|
68
72
|
};
|
|
73
|
+
var calculateSingularFixedPriceDiscount = (total, appraisals, coupons, stackable = false) => {
|
|
74
|
+
const singularFixedPriceDiscounts = coupons.filter(isFixedPriceCoupon).filter((coupon) => stackable ? isStackable(coupon) : !isStackable(coupon)).map((coupon) => coupon.amount).filter((amount) => amount > 0);
|
|
75
|
+
if (singularFixedPriceDiscounts.length === 0) return 0;
|
|
76
|
+
const lowestFixedPrice = Math.min(...singularFixedPriceDiscounts);
|
|
77
|
+
const reducedPrices = appraisals.map((appraisal) => (
|
|
78
|
+
// If the appraisal price is less than the fixed price
|
|
79
|
+
appraisal.price < lowestFixedPrice ? appraisal.price : lowestFixedPrice
|
|
80
|
+
));
|
|
81
|
+
const reducedTotal = reducedPrices.reduce((acc, price) => acc + price, 0);
|
|
82
|
+
const discount = total - reducedTotal;
|
|
83
|
+
return discount > 0 ? discount : 0;
|
|
84
|
+
};
|
|
69
85
|
|
|
70
86
|
// src/Discount/Diviner.ts
|
|
71
87
|
var DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS = {
|
|
@@ -82,33 +98,15 @@ var PaymentDiscountDiviner = class extends AbstractDiviner {
|
|
|
82
98
|
const terms = payloads.find(isEscrowTerms);
|
|
83
99
|
if (!terms) return [{ ...NO_DISCOUNT, sources }];
|
|
84
100
|
sources.push(await PayloadBuilder.hash(terms));
|
|
85
|
-
const discountHashes = terms.discounts ?? [];
|
|
86
|
-
if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }];
|
|
87
101
|
const termsAppraisals = terms?.appraisals;
|
|
88
102
|
if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }];
|
|
89
103
|
const hashMap = await PayloadBuilder.toAllHashMap(payloads);
|
|
90
|
-
const
|
|
91
|
-
sources.push(...
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const discounts = discountHashes.map((hash) => hashMap[hash]).filter(exists2).filter(isCoupon);
|
|
97
|
-
if (discounts.length !== discountHashes.length) {
|
|
98
|
-
const discountsArchivist = await this.getDiscountsArchivist();
|
|
99
|
-
const foundDiscounts = await discountsArchivist.get(discountHashes);
|
|
100
|
-
discounts.push(...foundDiscounts.filter(isCouponWithMeta));
|
|
101
|
-
}
|
|
102
|
-
const discountsMap = await PayloadBuilder.toAllHashMap(discounts);
|
|
103
|
-
if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }];
|
|
104
|
-
const foundDiscountsHashes = Object.keys(discountsMap);
|
|
105
|
-
sources.push(...foundDiscountsHashes);
|
|
106
|
-
for (const hash of discountHashes) {
|
|
107
|
-
if (!foundDiscountsHashes.includes(hash)) {
|
|
108
|
-
console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const coupons = Object.values(discountsMap);
|
|
104
|
+
const appraisals = this.getEscrowAppraisals(terms, hashMap);
|
|
105
|
+
sources.push(...termsAppraisals);
|
|
106
|
+
if (appraisals.length !== termsAppraisals.length) return [{ ...NO_DISCOUNT, sources }];
|
|
107
|
+
const coupons = await this.getEscrowDiscounts(terms, hashMap);
|
|
108
|
+
const couponHashes = await PayloadBuilder.hashes(coupons);
|
|
109
|
+
sources.push(...couponHashes);
|
|
112
110
|
const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent));
|
|
113
111
|
if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }];
|
|
114
112
|
const discount = applyCoupons(appraisals, validCoupons);
|
|
@@ -150,6 +148,42 @@ var PaymentDiscountDiviner = class extends AbstractDiviner {
|
|
|
150
148
|
const mod = assertEx2(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`);
|
|
151
149
|
return assertEx2(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`);
|
|
152
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Finds the appraisals specified by the escrow terms from the supplied payloads
|
|
153
|
+
* @param terms The escrow terms
|
|
154
|
+
* @param payloads The payloads to search for the appraisals
|
|
155
|
+
* @returns The appraisals found in the payloads
|
|
156
|
+
*/
|
|
157
|
+
getEscrowAppraisals(terms, hashMap) {
|
|
158
|
+
const hashes = terms?.appraisals ?? [];
|
|
159
|
+
if (hashes.length === 0) return [];
|
|
160
|
+
return hashes.map((hash) => hashMap[hash]).filter(exists2).filter(isHashLeaseEstimate);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Finds the discounts specified by the escrow terms from the supplied payloads
|
|
164
|
+
* @param terms The escrow terms
|
|
165
|
+
* @param hashMap The payloads to search for the discounts
|
|
166
|
+
* @returns The discounts found in the payloads
|
|
167
|
+
*/
|
|
168
|
+
async getEscrowDiscounts(terms, hashMap) {
|
|
169
|
+
const hashes = terms.discounts ?? [];
|
|
170
|
+
if (hashes.length === 0) return [];
|
|
171
|
+
const discounts = hashes.map((hash) => hashMap[hash]).filter(exists2).filter(isCoupon);
|
|
172
|
+
const missing = hashes.filter((hash) => !hashMap[hash]);
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
const discountsArchivist = await this.getDiscountsArchivist();
|
|
175
|
+
const payloads = await discountsArchivist.get(missing);
|
|
176
|
+
discounts.push(...payloads.filter(isCouponWithMeta));
|
|
177
|
+
}
|
|
178
|
+
if (discounts.length !== hashes.length) {
|
|
179
|
+
const termsHash = await PayloadBuilder.hash(terms);
|
|
180
|
+
const foundHashes = await PayloadBuilder.hashes(discounts);
|
|
181
|
+
for (const hash of hashes) {
|
|
182
|
+
if (!foundHashes.includes(hash)) console.warn(`Discount ${hash} not found for terms ${termsHash}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return discounts;
|
|
186
|
+
}
|
|
153
187
|
isCouponCurrent(coupon) {
|
|
154
188
|
const now = Date.now();
|
|
155
189
|
return coupon.exp > now && coupon.nbf < now;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Discount/Diviner.ts","../../src/Discount/lib/applyCoupons.ts","../../src/Invoice/getInvoiceForEscrow.ts","../../src/Subtotal/Diviner.ts","../../src/Subtotal/lib/appraisalValidators.ts","../../src/Subtotal/lib/durationValidators.ts","../../src/Subtotal/lib/termsValidators.ts","../../src/Total/Diviner.ts"],"sourcesContent":["import { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Address, Hash } from '@xylabs/hex'\nimport { ArchivistInstance, asArchivistInstance } from '@xyo-network/archivist-model'\nimport { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model'\nimport {\n HashLeaseEstimate,\n isHashLeaseEstimate,\n} from '@xyo-network/diviner-hash-lease'\nimport {\n asDivinerInstance, DivinerInstance, DivinerModuleEventData,\n} from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload } from '@xyo-network/payload-model'\nimport {\n Coupon,\n Discount,\n EscrowTerms, isCoupon,\n isCouponWithMeta,\n isEscrowTerms, NO_DISCOUNT, PaymentDiscountDivinerConfigSchema, PaymentDiscountDivinerParams,\n} from '@xyo-network/payment-payload-plugins'\n\nimport { applyCoupons } from './lib/index.ts'\n\nconst DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly<BoundWitnessDivinerQueryPayload> = {\n limit: 1,\n order: 'desc',\n schema: BoundWitnessDivinerQuerySchema,\n}\n\nexport type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload\n\n@creatableModule()\nexport class PaymentDiscountDiviner<\n TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams,\n TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType,\n TOut extends Discount = Discount,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentDiscountDivinerConfigSchema]\n static override defaultConfigSchema = PaymentDiscountDivinerConfigSchema\n\n protected get couponAuthorities(): Address[] {\n return [...(this.config.couponAuthorities ?? []), ...(this.params.couponAuthorities ?? [])]\n }\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n const sources: Hash[] = []\n\n // Parse terms\n const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined\n if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n sources.push(await PayloadBuilder.hash(terms))\n\n // Parse discounts\n const discountHashes = terms.discounts ?? []\n if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n\n // TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic\n // Parse appraisals\n const termsAppraisals = terms?.appraisals\n if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n const hashMap = await PayloadBuilder.toAllHashMap(payloads)\n const foundAppraisals = termsAppraisals.filter(hash => hashMap[hash])\n // Add the appraisals that were found to the sources\n sources.push(...foundAppraisals)\n // If not all appraisals are found, return no discount\n if (foundAppraisals.length !== termsAppraisals.length) {\n return [{ ...NO_DISCOUNT, sources }] as TOut[]\n }\n // TODO: Cast should not be required\n const appraisals = foundAppraisals.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[]\n\n // Use the supplied payloads to find the discounts\n const discounts = discountHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) as Coupon[]\n // Find any remaining coupons from the archivist\n if (discounts.length !== discountHashes.length) {\n // Find remaining from discounts archivist\n const discountsArchivist = await this.getDiscountsArchivist()\n const foundDiscounts = await discountsArchivist.get(discountHashes)\n discounts.push(...foundDiscounts.filter(isCouponWithMeta))\n }\n const discountsMap = await PayloadBuilder.toAllHashMap(discounts)\n if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n\n // Add the found discounts to the sources\n const foundDiscountsHashes = Object.keys(discountsMap) as Hash[]\n sources.push(...foundDiscountsHashes)\n\n // Log individual discounts that were not found\n for (const hash of discountHashes) {\n if (!foundDiscountsHashes.includes(hash)) {\n console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`)\n }\n }\n\n // Parse coupons\n const coupons = Object.values(discountsMap)\n const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent))\n if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n\n const discount = applyCoupons(appraisals, validCoupons)\n return [{ ...discount, sources }] as TOut[]\n }\n\n /**\n * Filters the supplied list of coupons to only those that are signed by\n * addresses specified in the couponAuthorities\n * @param coupons The list of coupons to filter\n * @returns The filtered list of coupons that are signed by the couponAuthorities\n */\n protected async filterToSigned(coupons: Coupon[]): Promise<Coupon[]> {\n const signed: Coupon[] = []\n const dataHashMap = await PayloadBuilder.toDataHashMap(coupons)\n const boundWitnessDiviner = await this.getDiscountsBoundWitnessDiviner()\n const hashes = Object.keys(dataHashMap)\n const addresses = this.couponAuthorities\n // TODO: Keep an in memory cache of the hashes queried and their results\n // to avoid querying the same hash multiple times\n await Promise.all(hashes.map((h) => {\n const hash = h as Hash\n return Promise.all(addresses.map(async (address) => {\n const query: BoundWitnessDivinerQueryPayload = {\n ...DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS, addresses: [address], payload_hashes: [hash],\n }\n const result = await boundWitnessDiviner.divine([query])\n if (result.length > 0) signed.push(dataHashMap[hash])\n }))\n }))\n return signed\n }\n\n protected async getDiscountsArchivist(): Promise<ArchivistInstance> {\n const name = assertEx(this.config.archivist, () => 'Missing archivist in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving archivist: ${name}`)\n return assertEx(asArchivistInstance(mod), () => `Resolved module ${mod.address} not a valid Archivist`)\n }\n\n protected async getDiscountsBoundWitnessDiviner(): Promise<DivinerInstance> {\n const name = assertEx(this.config.boundWitnessDiviner, () => 'Missing boundWitnessDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`)\n }\n\n protected isCouponCurrent(coupon: Coupon): boolean {\n const now = Date.now()\n return coupon.exp > now && coupon.nbf < now\n }\n}\n","import { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport type {\n AmountFields,\n Coupon, Discount, FixedAmountCoupon,\n FixedPercentageCoupon,\n} from '@xyo-network/payment-payload-plugins'\nimport {\n DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon,\n isStackable,\n} from '@xyo-network/payment-payload-plugins'\n\nexport const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => {\n // Ensure all appraisals and coupons are in USD\n const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD')\n assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD')\n const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial<AmountFields>)?.currency).filter(exists).every(currency => currency === 'USD')\n assertEx(allCouponsAreUSD, 'All coupons must be in USD')\n const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0)\n\n // Calculated non-stackable discount coupons\n const singularFixedDiscount = Math.max(...coupons\n .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon))\n .map(coupon => (coupon as FixedAmountCoupon).amount), 0)\n const singularPercentageDiscount = (Math.max(...coupons\n .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon))\n .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total\n\n // Calculate stackable discount coupons\n // First calculate the total discount from fixed amount coupons\n const stackedFixedDiscount = coupons\n .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon))\n .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0)\n // Then calculate the total discount from percentage coupons and apply\n // the percentage discount to the remaining total after fixed discounts\n const stackedPercentageDiscount = coupons\n .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon))\n .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount)\n // Sum all stackable discounts\n const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount\n\n // Find the best coupon(s) to apply\n const maxDiscount = Math.max(\n singularFixedDiscount,\n singularPercentageDiscount,\n stackedDiscount,\n 0,\n )\n\n // Ensure discount is not more than the total\n const amount = Math.min(maxDiscount, total)\n\n // Return single discount payload\n return {\n amount, schema: DiscountSchema, currency: 'USD',\n }\n}\n","import type { Hash } from '@xylabs/hex'\nimport type { DivinerInstance } from '@xyo-network/diviner-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport type { Payload } from '@xyo-network/payload-model'\nimport type {\n Discount, EscrowTerms, Invoice, Payment, Subtotal, Total,\n} from '@xyo-network/payment-payload-plugins'\nimport {\n isDiscount, isSubtotal, isTotal, PaymentSchema,\n} from '@xyo-network/payment-payload-plugins'\n\n/**\n * Validates the escrow terms to ensure they are valid for a purchase\n * @returns A payment if the terms are valid for a purchase, undefined otherwise\n */\nexport const getInvoiceForEscrow = async (\n terms: EscrowTerms,\n dataHashMap: Record<Hash, Payload>,\n paymentTotalDiviner: DivinerInstance,\n): Promise<Invoice | undefined> => {\n const payloads = Object.values(dataHashMap)\n const results = await paymentTotalDiviner.divine([terms, ...payloads])\n const subtotal = results.find(isSubtotal) as Subtotal | undefined\n const discount = results.find(isDiscount) as Discount | undefined\n const total = results.find(isTotal) as Total | undefined\n if (!subtotal || !total) return undefined\n const { amount, currency } = total\n if (currency !== 'USD') return undefined\n const sources = await getSources(terms, subtotal, total, discount)\n const payment: Payment = {\n amount, currency, schema: PaymentSchema, sources,\n }\n return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment]\n}\n\nconst getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise<Hash[]> => {\n const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total]\n return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p)))\n}\n","import { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload } from '@xyo-network/payload-model'\nimport {\n EscrowTerms, isEscrowTerms, PaymentSubtotalDivinerConfigSchema, PaymentSubtotalDivinerParams, Subtotal, SubtotalSchema,\n} from '@xyo-network/payment-payload-plugins'\n\nimport {\n appraisalValidators, termsValidators, ValidEscrowTerms,\n} from './lib/index.ts'\n\nconst currency = 'USD'\n\n/**\n * Escrow terms that contain all the valid fields for calculating a subtotal\n */\nexport type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload\n\n@creatableModule()\nexport class PaymentSubtotalDiviner<\n TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams,\n TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType,\n TOut extends Subtotal = Subtotal,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentSubtotalDivinerConfigSchema]\n static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n // Find the escrow terms\n const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined\n if (!terms) return []\n\n // Run all terms validations\n if (!termsValidators.every(validator => validator(terms))) return []\n const validTerms = terms as ValidEscrowTerms\n\n // Retrieve all appraisals from terms\n const hashMap = await PayloadBuilder.toAllHashMap(payloads)\n const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[]\n\n // Ensure all appraisals are present\n if (appraisals.length !== validTerms.appraisals.length) return []\n\n // Run all appraisal validations\n if (!appraisalValidators.every(validator => validator(appraisals))) return []\n const amount = calculateSubtotal(appraisals)\n const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals]\n return [{\n amount, currency, schema: SubtotalSchema, sources,\n }] as TOut[]\n }\n}\n\n// TODO: Add support for other currencies\nconst calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => {\n return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0)\n}\n","import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport { isIso4217CurrencyCode } from '@xyo-network/payment-payload-plugins'\n\nimport { validateDuration } from './durationValidators.ts'\n\nconst validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => {\n // Ensure all appraisals are numeric\n if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false\n // Ensure all appraisals are positive numbers\n if (appraisals.some(appraisal => appraisal.price < 0)) return false\n return true\n}\n\nconst validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => {\n // NOTE: Only supporting USD for now, the remaining checks are for future-proofing.\n if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false\n\n // Check every object in the array to ensure they all are in a supported currency.\n if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false\n\n return true\n}\n\nconst validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => {\n // Check if the array is empty or contains only one element, no need to compare.\n if (appraisals.length <= 1) return true\n\n // Get the currency of the first element to compare with others.\n const { currency } = appraisals[0]\n if (!currency) return false\n\n // Check every object in the array to ensure they all have the same currency.\n if (!appraisals.every(item => item.currency === currency)) return false\n\n return true\n}\n\nconst validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration)\n\nexport const appraisalValidators = [\n validateAppraisalAmount,\n validateAppraisalCurrency,\n validateAppraisalConsistentCurrency,\n validateAppraisalWindow,\n]\n","import type { DurationFields } from '@xyo-network/xns-record-payload-plugins'\n\nconst FIVE_MINUTES = 1000 * 60 * 5\n\n/**\n * Validates that the current time is within the duration window, within a configurable a buffer\n * @param value The duration value\n * @param windowMs The window in milliseconds to allow for a buffer\n * @returns True if the duration is valid, false otherwise\n */\nexport const validateDuration = (value: Partial<DurationFields>, windowMs = FIVE_MINUTES): boolean => {\n const now = Date.now()\n if (!value.nbf || value.nbf > now) return false\n // If already expired (include for a 5 minute buffer to allow for a reasonable\n // minimum amount of time for the transaction to be processed)\n if (!value.exp || value.exp - now < windowMs) return false\n return true\n}\n","import type { Hash } from '@xylabs/hex'\nimport type { EscrowTerms } from '@xyo-network/payment-payload-plugins'\n\nimport { validateDuration } from './durationValidators.ts'\n\nexport type ValidEscrowTerms = Required<EscrowTerms>\n\nconst validateTermsAppraisals = (terms: EscrowTerms): terms is Required<EscrowTerms & { appraisals: Hash[] }> => {\n if (!terms.appraisals) return false\n if (terms.appraisals.length === 0) return false\n return true\n}\nconst validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms)\n\nexport const termsValidators = [\n validateTermsAppraisals,\n validateTermsWindow,\n]\n","import { assertEx } from '@xylabs/assert'\nimport { Hash } from '@xylabs/hex'\nimport { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport {\n asDivinerInstance, DivinerInstance, DivinerModuleEventData,\n} from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport {\n Discount,\n isDiscountWithMeta,\n isSubtotalWithMeta,\n PaymentTotalDivinerConfigSchema, PaymentTotalDivinerParams, Subtotal, Total, TotalSchema,\n} from '@xyo-network/payment-payload-plugins'\n\nimport { PaymentDiscountDiviner, PaymentDiscountDivinerInputType } from '../Discount/index.ts'\nimport { PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType } from '../Subtotal/index.ts'\n\ntype InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType\ntype OutputType = Subtotal | Discount | Total\n\n@creatableModule()\nexport class PaymentTotalDiviner<\n TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams,\n TIn extends InputType = InputType,\n TOut extends OutputType = OutputType,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentTotalDivinerConfigSchema]\n static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n const subtotalDiviner = await this.getPaymentSubtotalDiviner()\n const subtotalResult = await subtotalDiviner.divine(payloads)\n const subtotal = subtotalResult.find(isSubtotalWithMeta)\n if (!subtotal) return []\n const discountDiviner = await this.getPaymentDiscountsDiviner()\n const discountResult = await discountDiviner.divine(payloads)\n const discount = discountResult.find(isDiscountWithMeta)\n if (!discount) return []\n const { currency: subtotalCurrency } = subtotal\n const { currency: discountCurrency } = discount\n assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`)\n const amount = Math.max(0, subtotal.amount - discount.amount)\n const currency = subtotalCurrency\n const sources = [subtotal.$hash, discount.$hash] as Hash[]\n const total: Total = {\n amount, currency, sources, schema: TotalSchema,\n }\n return [subtotal, discount, total] as TOut[]\n }\n\n protected async getPaymentDiscountsDiviner(): Promise<PaymentDiscountDiviner> {\n const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner\n }\n\n protected async getPaymentSubtotalDiviner(): Promise<PaymentSubtotalDiviner> {\n const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner\n }\n}\n"],"mappings":"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","names":["assertEx","exists","currency","exists","assertEx","PayloadBuilder","currency","AbstractDiviner","isHashLeaseEstimate","creatableModule","PayloadBuilder","isEscrowTerms","currency","AbstractDiviner","isEscrowTerms","PayloadBuilder","isHashLeaseEstimate","creatableModule","assertEx","AbstractDiviner","asDivinerInstance","creatableModule","AbstractDiviner","assertEx","currency","asDivinerInstance","creatableModule"]}
|
|
1
|
+
{"version":3,"sources":["../../src/Discount/Diviner.ts","../../src/Discount/lib/applyCoupons.ts","../../src/Invoice/getInvoiceForEscrow.ts","../../src/Subtotal/Diviner.ts","../../src/Subtotal/lib/appraisalValidators.ts","../../src/Subtotal/lib/durationValidators.ts","../../src/Subtotal/lib/termsValidators.ts","../../src/Total/Diviner.ts"],"sourcesContent":["import { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Address, Hash } from '@xylabs/hex'\nimport { ArchivistInstance, asArchivistInstance } from '@xyo-network/archivist-model'\nimport { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model'\nimport {\n HashLeaseEstimate,\n isHashLeaseEstimate,\n} from '@xyo-network/diviner-hash-lease'\nimport {\n asDivinerInstance, DivinerInstance, DivinerModuleEventData,\n} from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload } from '@xyo-network/payload-model'\nimport {\n Coupon,\n Discount,\n EscrowTerms, isCoupon,\n isCouponWithMeta,\n isEscrowTerms, NO_DISCOUNT, PaymentDiscountDivinerConfigSchema, PaymentDiscountDivinerParams,\n} from '@xyo-network/payment-payload-plugins'\n\nimport { applyCoupons } from './lib/index.ts'\n\nconst DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly<BoundWitnessDivinerQueryPayload> = {\n limit: 1,\n order: 'desc',\n schema: BoundWitnessDivinerQuerySchema,\n}\n\nexport type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload\n\n@creatableModule()\nexport class PaymentDiscountDiviner<\n TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams,\n TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType,\n TOut extends Discount = Discount,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentDiscountDivinerConfigSchema]\n static override defaultConfigSchema = PaymentDiscountDivinerConfigSchema\n\n protected get couponAuthorities(): Address[] {\n return [...(this.config.couponAuthorities ?? []), ...(this.params.couponAuthorities ?? [])]\n }\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n const sources: Hash[] = []\n\n // Parse terms\n const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined\n if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n sources.push(await PayloadBuilder.hash(terms))\n\n // Parse appraisals\n const termsAppraisals = terms?.appraisals\n // If the escrow terms do not have appraisals, return no discount\n if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n const hashMap = await PayloadBuilder.toAllHashMap(payloads) as Record<Hash, Payload>\n const appraisals = this.getEscrowAppraisals(terms, hashMap)\n // Add the appraisals that were found to the sources\n sources.push(...termsAppraisals)\n // If not all appraisals are found, return no discount\n if (appraisals.length !== termsAppraisals.length) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n\n // Parse coupons\n const coupons = await this.getEscrowDiscounts(terms, hashMap)\n // Add the coupons that were found to the sources\n // NOTE: Should we throw if not all coupons are found?\n const couponHashes = await PayloadBuilder.hashes(coupons)\n sources.push(...couponHashes)\n\n const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent))\n // NOTE: Should we throw if not all coupons are valid?\n if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]\n\n // TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic\n const discount = applyCoupons(appraisals, validCoupons)\n return [{ ...discount, sources }] as TOut[]\n }\n\n /**\n * Filters the supplied list of coupons to only those that are signed by\n * addresses specified in the couponAuthorities\n * @param coupons The list of coupons to filter\n * @returns The filtered list of coupons that are signed by the couponAuthorities\n */\n protected async filterToSigned(coupons: Coupon[]): Promise<Coupon[]> {\n const signed: Coupon[] = []\n const dataHashMap = await PayloadBuilder.toDataHashMap(coupons)\n const boundWitnessDiviner = await this.getDiscountsBoundWitnessDiviner()\n const hashes = Object.keys(dataHashMap)\n const addresses = this.couponAuthorities\n // TODO: Keep an in memory cache of the hashes queried and their results\n // to avoid querying the same hash multiple times\n await Promise.all(hashes.map((h) => {\n const hash = h as Hash\n return Promise.all(addresses.map(async (address) => {\n const query: BoundWitnessDivinerQueryPayload = {\n ...DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS, addresses: [address], payload_hashes: [hash],\n }\n const result = await boundWitnessDiviner.divine([query])\n if (result.length > 0) signed.push(dataHashMap[hash])\n }))\n }))\n return signed\n }\n\n protected async getDiscountsArchivist(): Promise<ArchivistInstance> {\n const name = assertEx(this.config.archivist, () => 'Missing archivist in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving archivist: ${name}`)\n return assertEx(asArchivistInstance(mod), () => `Resolved module ${mod.address} not a valid Archivist`)\n }\n\n protected async getDiscountsBoundWitnessDiviner(): Promise<DivinerInstance> {\n const name = assertEx(this.config.boundWitnessDiviner, () => 'Missing boundWitnessDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`)\n }\n\n /**\n * Finds the appraisals specified by the escrow terms from the supplied payloads\n * @param terms The escrow terms\n * @param payloads The payloads to search for the appraisals\n * @returns The appraisals found in the payloads\n */\n protected getEscrowAppraisals(terms: EscrowTerms, hashMap: Record<Hash, Payload>): HashLeaseEstimate[] {\n const hashes = terms?.appraisals ?? []\n if (hashes.length === 0) return []\n return hashes.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate)\n }\n\n /**\n * Finds the discounts specified by the escrow terms from the supplied payloads\n * @param terms The escrow terms\n * @param hashMap The payloads to search for the discounts\n * @returns The discounts found in the payloads\n */\n protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record<Hash, Payload>): Promise<Coupon[]> {\n // Parse discounts\n const hashes = terms.discounts ?? []\n if (hashes.length === 0) return []\n\n // Use the supplied payloads to find the discounts\n const discounts: Coupon[] = hashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon)\n const missing = hashes.filter(hash => !hashMap[hash])\n // If not all discounts are found\n if (missing.length > 0) {\n // Find any remaining from discounts archivist\n const discountsArchivist = await this.getDiscountsArchivist()\n const payloads = await discountsArchivist.get(missing)\n discounts.push(...payloads.filter(isCouponWithMeta))\n }\n // If not all discounts are found\n if (discounts.length !== hashes.length) {\n const termsHash = await PayloadBuilder.hash(terms)\n const foundHashes = await PayloadBuilder.hashes(discounts)\n // Log individual discounts that were not found\n for (const hash of hashes) {\n if (!foundHashes.includes(hash)) console.warn(`Discount ${hash} not found for terms ${termsHash}`)\n }\n }\n return discounts\n }\n\n protected isCouponCurrent(coupon: Coupon): boolean {\n const now = Date.now()\n return coupon.exp > now && coupon.nbf < now\n }\n}\n","import { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport type {\n AmountFields,\n Coupon, Discount, FixedAmountCoupon,\n FixedPercentageCoupon,\n FixedPriceCoupon,\n} from '@xyo-network/payment-payload-plugins'\nimport {\n DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon,\n isFixedPriceCoupon,\n isStackable,\n} from '@xyo-network/payment-payload-plugins'\n\nexport const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => {\n // Ensure all appraisals and coupons are in USD\n const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD')\n assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD')\n const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial<AmountFields>)?.currency).filter(exists).every(currency => currency === 'USD')\n assertEx(allCouponsAreUSD, 'All coupons must be in USD')\n const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0)\n\n // Calculated non-stackable discount coupons\n const singularFixedDiscount = Math.max(...coupons\n .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon))\n .map(coupon => (coupon as FixedAmountCoupon).amount), 0)\n const singularPercentageDiscount = (Math.max(...coupons\n .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon))\n .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total\n const singularFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, false)\n\n // Calculate stackable discount coupons\n // First calculate the total discount from fixed amount coupons\n const stackedFixedDiscount = coupons\n .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon))\n .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0)\n // Then calculate the total discount from percentage coupons and apply\n // the percentage discount to the remaining total after fixed discounts\n const stackedPercentageDiscount = coupons\n .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon))\n .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount)\n // Then calculate the total discount from fixed price coupons\n const stackedFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, true)\n\n // Sum all stackable discounts\n const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount + stackedFixedPriceDiscount\n\n // Find the best coupon(s) to apply\n const maxDiscount = Math.max(\n singularFixedDiscount,\n singularFixedPriceDiscount,\n singularPercentageDiscount,\n stackedDiscount,\n 0,\n )\n\n // Ensure discount is not more than the total\n const amount = Math.min(maxDiscount, total)\n\n // Return single discount payload\n return {\n amount, schema: DiscountSchema, currency: 'USD',\n }\n}\n\nconst calculateSingularFixedPriceDiscount = (total: number, appraisals: HashLeaseEstimate[], coupons: Coupon[], stackable = false): number => {\n // Find all singular fixed price discounts\n const singularFixedPriceDiscounts = coupons\n .filter(isFixedPriceCoupon)\n .filter(coupon => stackable ? isStackable(coupon) : !isStackable(coupon))\n .map(coupon => (coupon as FixedPriceCoupon).amount)\n // Ensure all fixed price discounts are positive\n .filter(amount => amount > 0)\n\n // If there are no singular fixed price discounts, return no discount\n if (singularFixedPriceDiscounts.length === 0) return 0\n\n // Find the maximum discount (the lowest fixed price)\n const lowestFixedPrice = Math.min(...singularFixedPriceDiscounts)\n\n // Apply the fixed price to all appraisals to get the reduced prices\n const reducedPrices = appraisals.map(appraisal =>\n // If the appraisal price is less than the fixed price\n appraisal.price < lowestFixedPrice\n // Use the appraisal price as the coupon will result in a higher price\n ? appraisal.price\n // Otherwise, apply the fixed price discount\n : lowestFixedPrice)\n\n // Calculate the reduced total using the reduced prices\n const reducedTotal = reducedPrices.reduce((acc, price) => acc + price, 0)\n\n // Calculate the discount\n const discount = total - reducedTotal\n\n // Return the discount or 0 if the discount would have resulted in a negative value\n return discount > 0 ? discount : 0\n}\n","import type { Hash } from '@xylabs/hex'\nimport type { DivinerInstance } from '@xyo-network/diviner-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport type { Payload } from '@xyo-network/payload-model'\nimport type {\n Discount, EscrowTerms, Invoice, Payment, Subtotal, Total,\n} from '@xyo-network/payment-payload-plugins'\nimport {\n isDiscount, isSubtotal, isTotal, PaymentSchema,\n} from '@xyo-network/payment-payload-plugins'\n\n/**\n * Validates the escrow terms to ensure they are valid for a purchase\n * @returns A payment if the terms are valid for a purchase, undefined otherwise\n */\nexport const getInvoiceForEscrow = async (\n terms: EscrowTerms,\n dataHashMap: Record<Hash, Payload>,\n paymentTotalDiviner: DivinerInstance,\n): Promise<Invoice | undefined> => {\n const payloads = Object.values(dataHashMap)\n const results = await paymentTotalDiviner.divine([terms, ...payloads])\n const subtotal = results.find(isSubtotal) as Subtotal | undefined\n const discount = results.find(isDiscount) as Discount | undefined\n const total = results.find(isTotal) as Total | undefined\n if (!subtotal || !total) return undefined\n const { amount, currency } = total\n if (currency !== 'USD') return undefined\n const sources = await getSources(terms, subtotal, total, discount)\n const payment: Payment = {\n amount, currency, schema: PaymentSchema, sources,\n }\n return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment]\n}\n\nconst getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise<Hash[]> => {\n const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total]\n return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p)))\n}\n","import { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload } from '@xyo-network/payload-model'\nimport {\n EscrowTerms, isEscrowTerms, PaymentSubtotalDivinerConfigSchema, PaymentSubtotalDivinerParams, Subtotal, SubtotalSchema,\n} from '@xyo-network/payment-payload-plugins'\n\nimport {\n appraisalValidators, termsValidators, ValidEscrowTerms,\n} from './lib/index.ts'\n\nconst currency = 'USD'\n\n/**\n * Escrow terms that contain all the valid fields for calculating a subtotal\n */\nexport type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload\n\n@creatableModule()\nexport class PaymentSubtotalDiviner<\n TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams,\n TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType,\n TOut extends Subtotal = Subtotal,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentSubtotalDivinerConfigSchema]\n static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n // Find the escrow terms\n const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined\n if (!terms) return []\n\n // Run all terms validations\n if (!termsValidators.every(validator => validator(terms))) return []\n const validTerms = terms as ValidEscrowTerms\n\n // Retrieve all appraisals from terms\n const hashMap = await PayloadBuilder.toAllHashMap(payloads)\n const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[]\n\n // Ensure all appraisals are present\n if (appraisals.length !== validTerms.appraisals.length) return []\n\n // Run all appraisal validations\n if (!appraisalValidators.every(validator => validator(appraisals))) return []\n const amount = calculateSubtotal(appraisals)\n const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals]\n return [{\n amount, currency, schema: SubtotalSchema, sources,\n }] as TOut[]\n }\n}\n\n// TODO: Add support for other currencies\nconst calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => {\n return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0)\n}\n","import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'\nimport { isIso4217CurrencyCode } from '@xyo-network/payment-payload-plugins'\n\nimport { validateDuration } from './durationValidators.ts'\n\nconst validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => {\n // Ensure all appraisals are numeric\n if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false\n // Ensure all appraisals are positive numbers\n if (appraisals.some(appraisal => appraisal.price < 0)) return false\n return true\n}\n\nconst validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => {\n // NOTE: Only supporting USD for now, the remaining checks are for future-proofing.\n if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false\n\n // Check every object in the array to ensure they all are in a supported currency.\n if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false\n\n return true\n}\n\nconst validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => {\n // Check if the array is empty or contains only one element, no need to compare.\n if (appraisals.length <= 1) return true\n\n // Get the currency of the first element to compare with others.\n const { currency } = appraisals[0]\n if (!currency) return false\n\n // Check every object in the array to ensure they all have the same currency.\n if (!appraisals.every(item => item.currency === currency)) return false\n\n return true\n}\n\nconst validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration)\n\nexport const appraisalValidators = [\n validateAppraisalAmount,\n validateAppraisalCurrency,\n validateAppraisalConsistentCurrency,\n validateAppraisalWindow,\n]\n","import type { DurationFields } from '@xyo-network/xns-record-payload-plugins'\n\nconst FIVE_MINUTES = 1000 * 60 * 5\n\n/**\n * Validates that the current time is within the duration window, within a configurable a buffer\n * @param value The duration value\n * @param windowMs The window in milliseconds to allow for a buffer\n * @returns True if the duration is valid, false otherwise\n */\nexport const validateDuration = (value: Partial<DurationFields>, windowMs = FIVE_MINUTES): boolean => {\n const now = Date.now()\n if (!value.nbf || value.nbf > now) return false\n // If already expired (include for a 5 minute buffer to allow for a reasonable\n // minimum amount of time for the transaction to be processed)\n if (!value.exp || value.exp - now < windowMs) return false\n return true\n}\n","import type { Hash } from '@xylabs/hex'\nimport type { EscrowTerms } from '@xyo-network/payment-payload-plugins'\n\nimport { validateDuration } from './durationValidators.ts'\n\nexport type ValidEscrowTerms = Required<EscrowTerms>\n\nconst validateTermsAppraisals = (terms: EscrowTerms): terms is Required<EscrowTerms & { appraisals: Hash[] }> => {\n if (!terms.appraisals) return false\n if (terms.appraisals.length === 0) return false\n return true\n}\nconst validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms)\n\nexport const termsValidators = [\n validateTermsAppraisals,\n validateTermsWindow,\n]\n","import { assertEx } from '@xylabs/assert'\nimport { Hash } from '@xylabs/hex'\nimport { AbstractDiviner } from '@xyo-network/diviner-abstract'\nimport {\n asDivinerInstance, DivinerInstance, DivinerModuleEventData,\n} from '@xyo-network/diviner-model'\nimport { creatableModule } from '@xyo-network/module-model'\nimport {\n Discount,\n isDiscountWithMeta,\n isSubtotalWithMeta,\n PaymentTotalDivinerConfigSchema, PaymentTotalDivinerParams, Subtotal, Total, TotalSchema,\n} from '@xyo-network/payment-payload-plugins'\n\nimport { PaymentDiscountDiviner, PaymentDiscountDivinerInputType } from '../Discount/index.ts'\nimport { PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType } from '../Subtotal/index.ts'\n\ntype InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType\ntype OutputType = Subtotal | Discount | Total\n\n@creatableModule()\nexport class PaymentTotalDiviner<\n TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams,\n TIn extends InputType = InputType,\n TOut extends OutputType = OutputType,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [PaymentTotalDivinerConfigSchema]\n static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema\n\n protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {\n const subtotalDiviner = await this.getPaymentSubtotalDiviner()\n const subtotalResult = await subtotalDiviner.divine(payloads)\n const subtotal = subtotalResult.find(isSubtotalWithMeta)\n if (!subtotal) return []\n const discountDiviner = await this.getPaymentDiscountsDiviner()\n const discountResult = await discountDiviner.divine(payloads)\n const discount = discountResult.find(isDiscountWithMeta)\n if (!discount) return []\n const { currency: subtotalCurrency } = subtotal\n const { currency: discountCurrency } = discount\n assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`)\n const amount = Math.max(0, subtotal.amount - discount.amount)\n const currency = subtotalCurrency\n const sources = [subtotal.$hash, discount.$hash] as Hash[]\n const total: Total = {\n amount, currency, sources, schema: TotalSchema,\n }\n return [subtotal, discount, total] as TOut[]\n }\n\n protected async getPaymentDiscountsDiviner(): Promise<PaymentDiscountDiviner> {\n const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner\n }\n\n protected async getPaymentSubtotalDiviner(): Promise<PaymentSubtotalDiviner> {\n const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config')\n const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`)\n return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,YAAAA,iBAAgB;AACzB,SAAS,UAAAC,eAAc;AAEvB,SAA4B,2BAA2B;AACvD,SAAS,uBAAuB;AAChC,SAA0C,sCAAsC;AAChF;AAAA,EAEE;AAAA,OACK;AACP;AAAA,EACE;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAE/B;AAAA,EAGe;AAAA,EACb;AAAA,EACA;AAAA,EAAe;AAAA,EAAa;AAAA,OACvB;;;ACtBP,SAAS,gBAAgB;AACzB,SAAS,cAAc;AAQvB;AAAA,EACE;AAAA,EAAgB;AAAA,EAAqB;AAAA,EACrC;AAAA,EACA;AAAA,OACK;AAEA,IAAM,eAAe,CAAC,YAAiC,YAAgC;AAE5F,QAAM,sBAAsB,WAAW,MAAM,eAAa,UAAU,aAAa,KAAK;AACtF,WAAS,qBAAqB,+BAA+B;AAC7D,QAAM,mBAAmB,QAAQ,IAAI,YAAW,QAAkC,QAAQ,EAAE,OAAO,MAAM,EAAE,MAAM,CAAAC,cAAYA,cAAa,KAAK;AAC/I,WAAS,kBAAkB,4BAA4B;AACvD,QAAM,QAAQ,WAAW,OAAO,CAAC,KAAK,cAAc,MAAM,UAAU,OAAO,CAAC;AAG5E,QAAM,wBAAwB,KAAK,IAAI,GAAG,QACvC,OAAO,YAAU,oBAAoB,MAAM,KAAK,CAAC,YAAY,MAAM,CAAC,EACpE,IAAI,YAAW,OAA6B,MAAM,GAAG,CAAC;AACzD,QAAM,6BAA8B,KAAK,IAAI,GAAG,QAC7C,OAAO,YAAU,wBAAwB,MAAM,KAAK,CAAC,YAAY,MAAM,CAAC,EACxE,IAAI,YAAW,OAAiC,UAAU,GAAG,CAAC,IAAK;AACtE,QAAM,6BAA6B,oCAAoC,OAAO,YAAY,SAAS,KAAK;AAIxG,QAAM,uBAAuB,QAC1B,OAAO,YAAU,oBAAoB,MAAM,KAAK,YAAY,MAAM,CAAC,EACnE,OAAO,CAAC,KAAK,WAAW,MAAO,OAA6B,QAAQ,CAAC;AAGxE,QAAM,4BAA4B,QAC/B,OAAO,YAAU,wBAAwB,MAAM,KAAK,YAAY,MAAM,CAAC,EACvE,OAAO,CAAC,KAAK,WAAW,MAAO,OAAiC,YAAY,CAAC,KAAK,QAAQ;AAE7F,QAAM,4BAA4B,oCAAoC,OAAO,YAAY,SAAS,IAAI;AAGtG,QAAM,kBAAkB,uBAAuB,4BAA4B;AAG3E,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAGA,QAAM,SAAS,KAAK,IAAI,aAAa,KAAK;AAG1C,SAAO;AAAA,IACL;AAAA,IAAQ,QAAQ;AAAA,IAAgB,UAAU;AAAA,EAC5C;AACF;AAEA,IAAM,sCAAsC,CAAC,OAAe,YAAiC,SAAmB,YAAY,UAAkB;AAE5I,QAAM,8BAA8B,QACjC,OAAO,kBAAkB,EACzB,OAAO,YAAU,YAAY,YAAY,MAAM,IAAI,CAAC,YAAY,MAAM,CAAC,EACvE,IAAI,YAAW,OAA4B,MAAM,EAEjD,OAAO,YAAU,SAAS,CAAC;AAG9B,MAAI,4BAA4B,WAAW,EAAG,QAAO;AAGrD,QAAM,mBAAmB,KAAK,IAAI,GAAG,2BAA2B;AAGhE,QAAM,gBAAgB,WAAW,IAAI;AAAA;AAAA,IAEnC,UAAU,QAAQ,mBAEd,UAAU,QAEV;AAAA,GAAgB;AAGtB,QAAM,eAAe,cAAc,OAAO,CAAC,KAAK,UAAU,MAAM,OAAO,CAAC;AAGxE,QAAM,WAAW,QAAQ;AAGzB,SAAO,WAAW,IAAI,WAAW;AACnC;;;ADxEA,IAAM,4CAAuF;AAAA,EAC3F,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;AAKO,IAAM,yBAAN,cASG,gBAAgD;AAAA,EAIxD,IAAc,oBAA+B;AAC3C,WAAO,CAAC,GAAI,KAAK,OAAO,qBAAqB,CAAC,GAAI,GAAI,KAAK,OAAO,qBAAqB,CAAC,CAAE;AAAA,EAC5F;AAAA,EAEA,MAAgB,cAAc,WAAkB,CAAC,GAAoB;AACnE,UAAM,UAAkB,CAAC;AAGzB,UAAM,QAAQ,SAAS,KAAK,aAAa;AACzC,QAAI,CAAC,MAAO,QAAO,CAAC,EAAE,GAAG,aAAa,QAAQ,CAAC;AAC/C,YAAQ,KAAK,MAAM,eAAe,KAAK,KAAK,CAAC;AAG7C,UAAM,kBAAkB,OAAO;AAE/B,QAAI,CAAC,mBAAmB,gBAAgB,WAAW,EAAG,QAAO,CAAC,EAAE,GAAG,aAAa,QAAQ,CAAC;AACzF,UAAM,UAAU,MAAM,eAAe,aAAa,QAAQ;AAC1D,UAAM,aAAa,KAAK,oBAAoB,OAAO,OAAO;AAE1D,YAAQ,KAAK,GAAG,eAAe;AAE/B,QAAI,WAAW,WAAW,gBAAgB,OAAQ,QAAO,CAAC,EAAE,GAAG,aAAa,QAAQ,CAAC;AAGrF,UAAM,UAAU,MAAM,KAAK,mBAAmB,OAAO,OAAO;AAG5D,UAAM,eAAe,MAAM,eAAe,OAAO,OAAO;AACxD,YAAQ,KAAK,GAAG,YAAY;AAE5B,UAAM,eAAe,MAAM,KAAK,eAAe,QAAQ,OAAO,KAAK,eAAe,CAAC;AAEnF,QAAI,aAAa,WAAW,EAAG,QAAO,CAAC,EAAE,GAAG,aAAa,QAAQ,CAAC;AAGlE,UAAM,WAAW,aAAa,YAAY,YAAY;AACtD,WAAO,CAAC,EAAE,GAAG,UAAU,QAAQ,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAgB,eAAe,SAAsC;AACnE,UAAM,SAAmB,CAAC;AAC1B,UAAM,cAAc,MAAM,eAAe,cAAc,OAAO;AAC9D,UAAM,sBAAsB,MAAM,KAAK,gCAAgC;AACvE,UAAM,SAAS,OAAO,KAAK,WAAW;AACtC,UAAM,YAAY,KAAK;AAGvB,UAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM;AAClC,YAAM,OAAO;AACb,aAAO,QAAQ,IAAI,UAAU,IAAI,OAAO,YAAY;AAClD,cAAM,QAAyC;AAAA,UAC7C,GAAG;AAAA,UAA2C,WAAW,CAAC,OAAO;AAAA,UAAG,gBAAgB,CAAC,IAAI;AAAA,QAC3F;AACA,cAAM,SAAS,MAAM,oBAAoB,OAAO,CAAC,KAAK,CAAC;AACvD,YAAI,OAAO,SAAS,EAAG,QAAO,KAAK,YAAY,IAAI,CAAC;AAAA,MACtD,CAAC,CAAC;AAAA,IACJ,CAAC,CAAC;AACF,WAAO;AAAA,EACT;AAAA,EAEA,MAAgB,wBAAoD;AAClE,UAAM,OAAOC,UAAS,KAAK,OAAO,WAAW,MAAM,6BAA6B;AAChF,UAAM,MAAMA,UAAS,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,8BAA8B,IAAI,EAAE;AACzF,WAAOA,UAAS,oBAAoB,GAAG,GAAG,MAAM,mBAAmB,IAAI,OAAO,wBAAwB;AAAA,EACxG;AAAA,EAEA,MAAgB,kCAA4D;AAC1E,UAAM,OAAOA,UAAS,KAAK,OAAO,qBAAqB,MAAM,uCAAuC;AACpG,UAAM,MAAMA,UAAS,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,wCAAwC,IAAI,EAAE;AACnG,WAAOA,UAAS,kBAAkB,GAAG,GAAG,MAAM,mBAAmB,IAAI,OAAO,sBAAsB;AAAA,EACpG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,oBAAoB,OAAoB,SAAqD;AACrG,UAAM,SAAS,OAAO,cAAc,CAAC;AACrC,QAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,WAAO,OAAO,IAAI,UAAQ,QAAQ,IAAI,CAAC,EAAE,OAAOC,OAAM,EAAE,OAAO,mBAAmB;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAgB,mBAAmB,OAAoB,SAAmD;AAExG,UAAM,SAAS,MAAM,aAAa,CAAC;AACnC,QAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AAGjC,UAAM,YAAsB,OAAO,IAAI,UAAQ,QAAQ,IAAI,CAAC,EAAE,OAAOA,OAAM,EAAE,OAAO,QAAQ;AAC5F,UAAM,UAAU,OAAO,OAAO,UAAQ,CAAC,QAAQ,IAAI,CAAC;AAEpD,QAAI,QAAQ,SAAS,GAAG;AAEtB,YAAM,qBAAqB,MAAM,KAAK,sBAAsB;AAC5D,YAAM,WAAW,MAAM,mBAAmB,IAAI,OAAO;AACrD,gBAAU,KAAK,GAAG,SAAS,OAAO,gBAAgB,CAAC;AAAA,IACrD;AAEA,QAAI,UAAU,WAAW,OAAO,QAAQ;AACtC,YAAM,YAAY,MAAM,eAAe,KAAK,KAAK;AACjD,YAAM,cAAc,MAAM,eAAe,OAAO,SAAS;AAEzD,iBAAW,QAAQ,QAAQ;AACzB,YAAI,CAAC,YAAY,SAAS,IAAI,EAAG,SAAQ,KAAK,YAAY,IAAI,wBAAwB,SAAS,EAAE;AAAA,MACnG;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEU,gBAAgB,QAAyB;AACjD,UAAM,MAAM,KAAK,IAAI;AACrB,WAAO,OAAO,MAAM,OAAO,OAAO,MAAM;AAAA,EAC1C;AACF;AAlIE,cAVW,wBAUK,iBAAgB,CAAC,kCAAkC;AACnE,cAXW,wBAWK,uBAAsB;AAX3B,yBAAN;AAAA,EADN,gBAAgB;AAAA,GACJ;;;AEjCb,SAAS,kBAAAC,uBAAsB;AAK/B;AAAA,EACE;AAAA,EAAY;AAAA,EAAY;AAAA,EAAS;AAAA,OAC5B;AAMA,IAAM,sBAAsB,OACjC,OACA,aACA,wBACiC;AACjC,QAAM,WAAW,OAAO,OAAO,WAAW;AAC1C,QAAM,UAAU,MAAM,oBAAoB,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC;AACrE,QAAM,WAAW,QAAQ,KAAK,UAAU;AACxC,QAAM,WAAW,QAAQ,KAAK,UAAU;AACxC,QAAM,QAAQ,QAAQ,KAAK,OAAO;AAClC,MAAI,CAAC,YAAY,CAAC,MAAO,QAAO;AAChC,QAAM,EAAE,QAAQ,UAAAC,UAAS,IAAI;AAC7B,MAAIA,cAAa,MAAO,QAAO;AAC/B,QAAM,UAAU,MAAM,WAAW,OAAO,UAAU,OAAO,QAAQ;AACjE,QAAM,UAAmB;AAAA,IACvB;AAAA,IAAQ,UAAAA;AAAA,IAAU,QAAQ;AAAA,IAAe;AAAA,EAC3C;AACA,SAAO,WAAW,CAAC,UAAU,OAAO,SAAS,QAAQ,IAAI,CAAC,UAAU,OAAO,OAAO;AACpF;AAEA,IAAM,aAAa,OAAO,OAAoB,UAAoB,OAAc,aAAyC;AACvH,QAAM,UAAU,WAAW,CAAC,OAAO,UAAU,OAAO,QAAQ,IAAI,CAAC,OAAO,UAAU,KAAK;AACvF,SAAO,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAKD,gBAAe,SAAS,CAAC,CAAC,CAAC;AACvE;;;ACtCA,SAAS,mBAAAE,wBAAuB;AAChC,SAA4B,uBAAAC,4BAA2B;AAEvD,SAAS,mBAAAC,wBAAuB;AAChC,SAAS,kBAAAC,uBAAsB;AAE/B;AAAA,EACe,iBAAAC;AAAA,EAAe;AAAA,EAA4E;AAAA,OACnG;;;ACPP,SAAS,6BAA6B;;;ACCtC,IAAM,eAAe,MAAO,KAAK;AAQ1B,IAAM,mBAAmB,CAAC,OAAgC,WAAW,iBAA0B;AACpG,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,CAAC,MAAM,OAAO,MAAM,MAAM,IAAK,QAAO;AAG1C,MAAI,CAAC,MAAM,OAAO,MAAM,MAAM,MAAM,SAAU,QAAO;AACrD,SAAO;AACT;;;ADZA,IAAM,0BAA0B,CAAC,eAA6C;AAE5E,MAAI,WAAW,KAAK,eAAa,OAAO,UAAU,UAAU,QAAQ,EAAG,QAAO;AAE9E,MAAI,WAAW,KAAK,eAAa,UAAU,QAAQ,CAAC,EAAG,QAAO;AAC9D,SAAO;AACT;AAEA,IAAM,4BAA4B,CAAC,eAA6C;AAE9E,MAAI,CAAC,WAAW,MAAM,eAAa,UAAU,YAAY,KAAK,EAAG,QAAO;AAGxE,MAAI,CAAC,WAAW,MAAM,eAAa,sBAAsB,UAAU,QAAQ,CAAC,EAAG,QAAO;AAEtF,SAAO;AACT;AAEA,IAAM,sCAAsC,CAAC,eAA6C;AAExF,MAAI,WAAW,UAAU,EAAG,QAAO;AAGnC,QAAM,EAAE,UAAAC,UAAS,IAAI,WAAW,CAAC;AACjC,MAAI,CAACA,UAAU,QAAO;AAGtB,MAAI,CAAC,WAAW,MAAM,UAAQ,KAAK,aAAaA,SAAQ,EAAG,QAAO;AAElE,SAAO;AACT;AAEA,IAAM,0BAA0B,CAAC,eAA6C,WAAW,MAAM,gBAAgB;AAExG,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AErCA,IAAM,0BAA0B,CAAC,UAAgF;AAC/G,MAAI,CAAC,MAAM,WAAY,QAAO;AAC9B,MAAI,MAAM,WAAW,WAAW,EAAG,QAAO;AAC1C,SAAO;AACT;AACA,IAAM,sBAAsB,CAAC,UAAgC,iBAAiB,KAAK;AAE5E,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;;;AHHA,IAAM,WAAW;AAQV,IAAM,yBAAN,cASGC,iBAAgD;AAAA,EAIxD,MAAgB,cAAc,WAAkB,CAAC,GAAoB;AAEnE,UAAM,QAAQ,SAAS,KAAKC,cAAa;AACzC,QAAI,CAAC,MAAO,QAAO,CAAC;AAGpB,QAAI,CAAC,gBAAgB,MAAM,eAAa,UAAU,KAAK,CAAC,EAAG,QAAO,CAAC;AACnE,UAAM,aAAa;AAGnB,UAAM,UAAU,MAAMC,gBAAe,aAAa,QAAQ;AAC1D,UAAM,aAAa,WAAW,WAAW,IAAI,eAAa,QAAQ,SAAS,CAAC,EAAE,OAAOC,oBAAmB;AAGxG,QAAI,WAAW,WAAW,WAAW,WAAW,OAAQ,QAAO,CAAC;AAGhE,QAAI,CAAC,oBAAoB,MAAM,eAAa,UAAU,UAAU,CAAC,EAAG,QAAO,CAAC;AAC5E,UAAM,SAAS,kBAAkB,UAAU;AAC3C,UAAM,UAAU,CAAC,MAAMD,gBAAe,SAAS,UAAU,GAAG,GAAG,WAAW,UAAU;AACpF,WAAO,CAAC;AAAA,MACN;AAAA,MAAQ;AAAA,MAAU,QAAQ;AAAA,MAAgB;AAAA,IAC5C,CAAC;AAAA,EACH;AACF;AA3BE,cAVW,wBAUK,iBAAgB,CAAC,kCAAkC;AACnE,cAXW,wBAWK,uBAAsB;AAX3B,yBAAN;AAAA,EADNE,iBAAgB;AAAA,GACJ;AAwCb,IAAM,oBAAoB,CAAC,eAA4C;AACrE,SAAO,WAAW,OAAO,CAAC,KAAK,cAAc,MAAM,UAAU,OAAO,CAAC;AACvE;;;AIhEA,SAAS,YAAAC,iBAAgB;AAEzB,SAAS,mBAAAC,wBAAuB;AAChC;AAAA,EACE,qBAAAC;AAAA,OACK;AACP,SAAS,mBAAAC,wBAAuB;AAChC;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EAA6E;AAAA,OACxE;AASA,IAAM,sBAAN,cASGC,iBAAgD;AAAA,EAIxD,MAAgB,cAAc,WAAkB,CAAC,GAAoB;AACnE,UAAM,kBAAkB,MAAM,KAAK,0BAA0B;AAC7D,UAAM,iBAAiB,MAAM,gBAAgB,OAAO,QAAQ;AAC5D,UAAM,WAAW,eAAe,KAAK,kBAAkB;AACvD,QAAI,CAAC,SAAU,QAAO,CAAC;AACvB,UAAM,kBAAkB,MAAM,KAAK,2BAA2B;AAC9D,UAAM,iBAAiB,MAAM,gBAAgB,OAAO,QAAQ;AAC5D,UAAM,WAAW,eAAe,KAAK,kBAAkB;AACvD,QAAI,CAAC,SAAU,QAAO,CAAC;AACvB,UAAM,EAAE,UAAU,iBAAiB,IAAI;AACvC,UAAM,EAAE,UAAU,iBAAiB,IAAI;AACvC,IAAAC,UAAS,qBAAqB,kBAAkB,MAAM,qBAAqB,gBAAgB,qCAAqC,gBAAgB,EAAE;AAClJ,UAAM,SAAS,KAAK,IAAI,GAAG,SAAS,SAAS,SAAS,MAAM;AAC5D,UAAMC,YAAW;AACjB,UAAM,UAAU,CAAC,SAAS,OAAO,SAAS,KAAK;AAC/C,UAAM,QAAe;AAAA,MACnB;AAAA,MAAQ,UAAAA;AAAA,MAAU;AAAA,MAAS,QAAQ;AAAA,IACrC;AACA,WAAO,CAAC,UAAU,UAAU,KAAK;AAAA,EACnC;AAAA,EAEA,MAAgB,6BAA8D;AAC5E,UAAM,OAAOD,UAAS,KAAK,OAAO,wBAAwB,MAAM,0CAA0C;AAC1G,UAAM,MAAMA,UAAS,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,2CAA2C,IAAI,EAAE;AACtG,WAAOA,UAASE,mBAAkB,GAAG,GAAG,MAAM,mBAAmB,IAAI,OAAO,sBAAsB;AAAA,EACpG;AAAA,EAEA,MAAgB,4BAA6D;AAC3E,UAAM,OAAOF,UAAS,KAAK,OAAO,wBAAwB,MAAM,0CAA0C;AAC1G,UAAM,MAAMA,UAAS,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,2CAA2C,IAAI,EAAE;AACtG,WAAOA,UAASE,mBAAkB,GAAG,GAAG,MAAM,mBAAmB,IAAI,OAAO,sBAAsB;AAAA,EACpG;AACF;AAnCE,cAVW,qBAUK,iBAAgB,CAAC,+BAA+B;AAChE,cAXW,qBAWK,uBAAuD;AAX5D,sBAAN;AAAA,EADNC,iBAAgB;AAAA,GACJ;","names":["assertEx","exists","currency","assertEx","exists","PayloadBuilder","currency","AbstractDiviner","isHashLeaseEstimate","creatableModule","PayloadBuilder","isEscrowTerms","currency","AbstractDiviner","isEscrowTerms","PayloadBuilder","isHashLeaseEstimate","creatableModule","assertEx","AbstractDiviner","asDivinerInstance","creatableModule","AbstractDiviner","assertEx","currency","asDivinerInstance","creatableModule"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyo-network/payment-plugin",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.19",
|
|
4
4
|
"description": "Typescript/Javascript Plugins for XYO Platform",
|
|
5
5
|
"homepage": "https://xyo.network",
|
|
6
6
|
"bugs": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@xyo-network/module-model": "^3.1.9",
|
|
41
41
|
"@xyo-network/payload-builder": "^3.1.9",
|
|
42
42
|
"@xyo-network/payload-model": "^3.1.9",
|
|
43
|
-
"@xyo-network/payment-payload-plugins": "^3.0.
|
|
44
|
-
"@xyo-network/xns-record-payload-plugins": "^3.0.
|
|
43
|
+
"@xyo-network/payment-payload-plugins": "^3.0.19",
|
|
44
|
+
"@xyo-network/xns-record-payload-plugins": "^3.0.19"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@xylabs/ts-scripts-yarn3": "^4.0.7",
|
package/src/Discount/Diviner.ts
CHANGED
|
@@ -58,53 +58,29 @@ export class PaymentDiscountDiviner<
|
|
|
58
58
|
if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
59
59
|
sources.push(await PayloadBuilder.hash(terms))
|
|
60
60
|
|
|
61
|
-
// Parse discounts
|
|
62
|
-
const discountHashes = terms.discounts ?? []
|
|
63
|
-
if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
64
|
-
|
|
65
|
-
// TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic
|
|
66
61
|
// Parse appraisals
|
|
67
62
|
const termsAppraisals = terms?.appraisals
|
|
63
|
+
// If the escrow terms do not have appraisals, return no discount
|
|
68
64
|
if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
69
|
-
const hashMap = await PayloadBuilder.toAllHashMap(payloads)
|
|
70
|
-
const
|
|
65
|
+
const hashMap = await PayloadBuilder.toAllHashMap(payloads) as Record<Hash, Payload>
|
|
66
|
+
const appraisals = this.getEscrowAppraisals(terms, hashMap)
|
|
71
67
|
// Add the appraisals that were found to the sources
|
|
72
|
-
sources.push(...
|
|
68
|
+
sources.push(...termsAppraisals)
|
|
73
69
|
// If not all appraisals are found, return no discount
|
|
74
|
-
if (
|
|
75
|
-
return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
76
|
-
}
|
|
77
|
-
// TODO: Cast should not be required
|
|
78
|
-
const appraisals = foundAppraisals.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[]
|
|
79
|
-
|
|
80
|
-
// Use the supplied payloads to find the discounts
|
|
81
|
-
const discounts = discountHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) as Coupon[]
|
|
82
|
-
// Find any remaining coupons from the archivist
|
|
83
|
-
if (discounts.length !== discountHashes.length) {
|
|
84
|
-
// Find remaining from discounts archivist
|
|
85
|
-
const discountsArchivist = await this.getDiscountsArchivist()
|
|
86
|
-
const foundDiscounts = await discountsArchivist.get(discountHashes)
|
|
87
|
-
discounts.push(...foundDiscounts.filter(isCouponWithMeta))
|
|
88
|
-
}
|
|
89
|
-
const discountsMap = await PayloadBuilder.toAllHashMap(discounts)
|
|
90
|
-
if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
91
|
-
|
|
92
|
-
// Add the found discounts to the sources
|
|
93
|
-
const foundDiscountsHashes = Object.keys(discountsMap) as Hash[]
|
|
94
|
-
sources.push(...foundDiscountsHashes)
|
|
95
|
-
|
|
96
|
-
// Log individual discounts that were not found
|
|
97
|
-
for (const hash of discountHashes) {
|
|
98
|
-
if (!foundDiscountsHashes.includes(hash)) {
|
|
99
|
-
console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
70
|
+
if (appraisals.length !== termsAppraisals.length) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
102
71
|
|
|
103
72
|
// Parse coupons
|
|
104
|
-
const coupons =
|
|
73
|
+
const coupons = await this.getEscrowDiscounts(terms, hashMap)
|
|
74
|
+
// Add the coupons that were found to the sources
|
|
75
|
+
// NOTE: Should we throw if not all coupons are found?
|
|
76
|
+
const couponHashes = await PayloadBuilder.hashes(coupons)
|
|
77
|
+
sources.push(...couponHashes)
|
|
78
|
+
|
|
105
79
|
const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent))
|
|
80
|
+
// NOTE: Should we throw if not all coupons are valid?
|
|
106
81
|
if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[]
|
|
107
82
|
|
|
83
|
+
// TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic
|
|
108
84
|
const discount = applyCoupons(appraisals, validCoupons)
|
|
109
85
|
return [{ ...discount, sources }] as TOut[]
|
|
110
86
|
}
|
|
@@ -148,6 +124,51 @@ export class PaymentDiscountDiviner<
|
|
|
148
124
|
return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`)
|
|
149
125
|
}
|
|
150
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Finds the appraisals specified by the escrow terms from the supplied payloads
|
|
129
|
+
* @param terms The escrow terms
|
|
130
|
+
* @param payloads The payloads to search for the appraisals
|
|
131
|
+
* @returns The appraisals found in the payloads
|
|
132
|
+
*/
|
|
133
|
+
protected getEscrowAppraisals(terms: EscrowTerms, hashMap: Record<Hash, Payload>): HashLeaseEstimate[] {
|
|
134
|
+
const hashes = terms?.appraisals ?? []
|
|
135
|
+
if (hashes.length === 0) return []
|
|
136
|
+
return hashes.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Finds the discounts specified by the escrow terms from the supplied payloads
|
|
141
|
+
* @param terms The escrow terms
|
|
142
|
+
* @param hashMap The payloads to search for the discounts
|
|
143
|
+
* @returns The discounts found in the payloads
|
|
144
|
+
*/
|
|
145
|
+
protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record<Hash, Payload>): Promise<Coupon[]> {
|
|
146
|
+
// Parse discounts
|
|
147
|
+
const hashes = terms.discounts ?? []
|
|
148
|
+
if (hashes.length === 0) return []
|
|
149
|
+
|
|
150
|
+
// Use the supplied payloads to find the discounts
|
|
151
|
+
const discounts: Coupon[] = hashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon)
|
|
152
|
+
const missing = hashes.filter(hash => !hashMap[hash])
|
|
153
|
+
// If not all discounts are found
|
|
154
|
+
if (missing.length > 0) {
|
|
155
|
+
// Find any remaining from discounts archivist
|
|
156
|
+
const discountsArchivist = await this.getDiscountsArchivist()
|
|
157
|
+
const payloads = await discountsArchivist.get(missing)
|
|
158
|
+
discounts.push(...payloads.filter(isCouponWithMeta))
|
|
159
|
+
}
|
|
160
|
+
// If not all discounts are found
|
|
161
|
+
if (discounts.length !== hashes.length) {
|
|
162
|
+
const termsHash = await PayloadBuilder.hash(terms)
|
|
163
|
+
const foundHashes = await PayloadBuilder.hashes(discounts)
|
|
164
|
+
// Log individual discounts that were not found
|
|
165
|
+
for (const hash of hashes) {
|
|
166
|
+
if (!foundHashes.includes(hash)) console.warn(`Discount ${hash} not found for terms ${termsHash}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return discounts
|
|
170
|
+
}
|
|
171
|
+
|
|
151
172
|
protected isCouponCurrent(coupon: Coupon): boolean {
|
|
152
173
|
const now = Date.now()
|
|
153
174
|
return coupon.exp > now && coupon.nbf < now
|
|
@@ -5,9 +5,11 @@ import type {
|
|
|
5
5
|
AmountFields,
|
|
6
6
|
Coupon, Discount, FixedAmountCoupon,
|
|
7
7
|
FixedPercentageCoupon,
|
|
8
|
+
FixedPriceCoupon,
|
|
8
9
|
} from '@xyo-network/payment-payload-plugins'
|
|
9
10
|
import {
|
|
10
11
|
DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon,
|
|
12
|
+
isFixedPriceCoupon,
|
|
11
13
|
isStackable,
|
|
12
14
|
} from '@xyo-network/payment-payload-plugins'
|
|
13
15
|
|
|
@@ -26,6 +28,7 @@ export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[])
|
|
|
26
28
|
const singularPercentageDiscount = (Math.max(...coupons
|
|
27
29
|
.filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon))
|
|
28
30
|
.map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total
|
|
31
|
+
const singularFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, false)
|
|
29
32
|
|
|
30
33
|
// Calculate stackable discount coupons
|
|
31
34
|
// First calculate the total discount from fixed amount coupons
|
|
@@ -37,12 +40,16 @@ export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[])
|
|
|
37
40
|
const stackedPercentageDiscount = coupons
|
|
38
41
|
.filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon))
|
|
39
42
|
.reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount)
|
|
43
|
+
// Then calculate the total discount from fixed price coupons
|
|
44
|
+
const stackedFixedPriceDiscount = calculateSingularFixedPriceDiscount(total, appraisals, coupons, true)
|
|
45
|
+
|
|
40
46
|
// Sum all stackable discounts
|
|
41
|
-
const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount
|
|
47
|
+
const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount + stackedFixedPriceDiscount
|
|
42
48
|
|
|
43
49
|
// Find the best coupon(s) to apply
|
|
44
50
|
const maxDiscount = Math.max(
|
|
45
51
|
singularFixedDiscount,
|
|
52
|
+
singularFixedPriceDiscount,
|
|
46
53
|
singularPercentageDiscount,
|
|
47
54
|
stackedDiscount,
|
|
48
55
|
0,
|
|
@@ -56,3 +63,37 @@ export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[])
|
|
|
56
63
|
amount, schema: DiscountSchema, currency: 'USD',
|
|
57
64
|
}
|
|
58
65
|
}
|
|
66
|
+
|
|
67
|
+
const calculateSingularFixedPriceDiscount = (total: number, appraisals: HashLeaseEstimate[], coupons: Coupon[], stackable = false): number => {
|
|
68
|
+
// Find all singular fixed price discounts
|
|
69
|
+
const singularFixedPriceDiscounts = coupons
|
|
70
|
+
.filter(isFixedPriceCoupon)
|
|
71
|
+
.filter(coupon => stackable ? isStackable(coupon) : !isStackable(coupon))
|
|
72
|
+
.map(coupon => (coupon as FixedPriceCoupon).amount)
|
|
73
|
+
// Ensure all fixed price discounts are positive
|
|
74
|
+
.filter(amount => amount > 0)
|
|
75
|
+
|
|
76
|
+
// If there are no singular fixed price discounts, return no discount
|
|
77
|
+
if (singularFixedPriceDiscounts.length === 0) return 0
|
|
78
|
+
|
|
79
|
+
// Find the maximum discount (the lowest fixed price)
|
|
80
|
+
const lowestFixedPrice = Math.min(...singularFixedPriceDiscounts)
|
|
81
|
+
|
|
82
|
+
// Apply the fixed price to all appraisals to get the reduced prices
|
|
83
|
+
const reducedPrices = appraisals.map(appraisal =>
|
|
84
|
+
// If the appraisal price is less than the fixed price
|
|
85
|
+
appraisal.price < lowestFixedPrice
|
|
86
|
+
// Use the appraisal price as the coupon will result in a higher price
|
|
87
|
+
? appraisal.price
|
|
88
|
+
// Otherwise, apply the fixed price discount
|
|
89
|
+
: lowestFixedPrice)
|
|
90
|
+
|
|
91
|
+
// Calculate the reduced total using the reduced prices
|
|
92
|
+
const reducedTotal = reducedPrices.reduce((acc, price) => acc + price, 0)
|
|
93
|
+
|
|
94
|
+
// Calculate the discount
|
|
95
|
+
const discount = total - reducedTotal
|
|
96
|
+
|
|
97
|
+
// Return the discount or 0 if the discount would have resulted in a negative value
|
|
98
|
+
return discount > 0 ? discount : 0
|
|
99
|
+
}
|