From 6911db4c7135191c79ed2c6989f9ec42b4ff7863 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 07:53:29 -0500 Subject: [PATCH 1/7] Filter coupons by conditions met --- .../packages/payments/src/Discount/Diviner.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts index f8895e40a..2fd25c14f 100644 --- a/packages/payloadset/packages/payments/src/Discount/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -22,7 +22,7 @@ import { isEscrowTerms, NO_DISCOUNT, PaymentDiscountDivinerConfigSchema, PaymentDiscountDivinerParams, } from '@xyo-network/payment-payload-plugins' -import { applyCoupons } from './lib/index.ts' +import { applyCoupons, areConditionsFulfilled } from './lib/index.ts' const DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly = { limit: 1, @@ -70,13 +70,17 @@ export class PaymentDiscountDiviner< if (appraisals.length !== termsAppraisals.length) return [{ ...NO_DISCOUNT, sources }] as TOut[] // Parse coupons - const coupons = await this.getEscrowDiscounts(terms, hashMap) + const [coupons] = await this.getEscrowDiscounts(terms, hashMap) // Add the coupons that were found to the sources // NOTE: Should we throw if not all coupons are found? const couponHashes = await PayloadBuilder.hashes(coupons) sources.push(...couponHashes) - const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent)) + const validCoupons = await this.filterToSigned( + coupons + .filter(this.isCouponCurrent) + .filter(coupon => areConditionsFulfilled(coupon, payloads)), + ) // NOTE: Should we throw if not all coupons are valid? if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] @@ -142,10 +146,10 @@ export class PaymentDiscountDiviner< * @param hashMap The payloads to search for the discounts * @returns The discounts found in the payloads */ - protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record): Promise { + protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record): Promise<[Coupon[]]> { // Parse discounts const hashes = terms.discounts ?? [] - if (hashes.length === 0) return [] + if (hashes.length === 0) return [[]] // Use the supplied payloads to find the discounts const discounts: Coupon[] = hashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) @@ -166,7 +170,9 @@ export class PaymentDiscountDiviner< if (!foundHashes.includes(hash)) console.warn(`Discount ${hash} not found for terms ${termsHash}`) } } - return discounts + + // TODO: Find all non-supplied coupon conditions here as well + return [discounts] } protected isCouponCurrent(coupon: Coupon): boolean { From 19f161095ea01163ecfd513043d87345c53603fe Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:08:29 -0500 Subject: [PATCH 2/7] Add types for coupon conditions --- .../payload/packages/payments/package.json | 1 + .../Payload/Coupon/types/Condition.ts | 24 +++++++++++++++++++ .../Discount/Payload/Coupon/types/index.ts | 1 + yarn.lock | 1 + 4 files changed, 27 insertions(+) create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/types/Condition.ts diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index cfd1763c8..6a7ad8cc5 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -41,6 +41,7 @@ "@xyo-network/id-payload-plugin": "^3.1.11", "@xyo-network/module-model": "^3.1.11", "@xyo-network/payload-model": "^3.1.11", + "@xyo-network/schema-payload-plugin": "^3.1.11", "@xyo-network/xns-record-payload-plugins": "workspace:^" }, "devDependencies": { diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/Condition.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/Condition.ts new file mode 100644 index 000000000..aa99b0c5e --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/Condition.ts @@ -0,0 +1,24 @@ +import { + isPayloadOfSchemaType, isPayloadOfSchemaTypeWithMeta, isPayloadOfSchemaTypeWithSources, type WithMeta, type WithOptionalMeta, +} from '@xyo-network/payload-model' +import { type SchemaPayload, SchemaSchema } from '@xyo-network/schema-payload-plugin' + +/** + * The payloads that can be used as conditions for a coupon + */ +export type Condition = SchemaPayload | WithOptionalMeta | WithMeta + +/** + * Identity function for determining if an object is a Condition payload + */ +export const isCondition = isPayloadOfSchemaType(SchemaSchema) + +/** + * Identity function for determining if an object is a Condition payload with sources + */ +export const isConditionWithSources = isPayloadOfSchemaTypeWithSources(SchemaSchema) + +/** + * Identity function for determining if an object is a Condition payload with meta + */ +export const isConditionWithMeta = isPayloadOfSchemaTypeWithMeta(SchemaSchema) diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts index ee5290191..9b502780a 100644 --- a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts @@ -1,2 +1,3 @@ +export * from './Condition.ts' export * from './CouponFields.ts' export * from './isStackable.ts' diff --git a/yarn.lock b/yarn.lock index 3df01608b..894ed7ce8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7094,6 +7094,7 @@ __metadata: "@xyo-network/id-payload-plugin": "npm:^3.1.11" "@xyo-network/module-model": "npm:^3.1.11" "@xyo-network/payload-model": "npm:^3.1.11" + "@xyo-network/schema-payload-plugin": "npm:^3.1.11" "@xyo-network/xns-record-payload-plugins": "workspace:^" typescript: "npm:^5.5.4" languageName: unknown From b325c0ab2a52a81c6e390a4c1dd5ebe55b12144f Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:14:37 -0500 Subject: [PATCH 3/7] Injection conditions --- .../packages/payments/src/Discount/Diviner.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts index 2fd25c14f..4712a4715 100644 --- a/packages/payloadset/packages/payments/src/Discount/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -15,6 +15,7 @@ import { creatableModule } from '@xyo-network/module-model' import { PayloadBuilder } from '@xyo-network/payload-builder' import { Payload } from '@xyo-network/payload-model' import { + Condition, Coupon, Discount, EscrowTerms, isCoupon, @@ -70,16 +71,17 @@ export class PaymentDiscountDiviner< if (appraisals.length !== termsAppraisals.length) return [{ ...NO_DISCOUNT, sources }] as TOut[] // Parse coupons - const [coupons] = await this.getEscrowDiscounts(terms, hashMap) + const [coupons, conditions] = await this.getEscrowDiscounts(terms, hashMap) // Add the coupons that were found to the sources // NOTE: Should we throw if not all coupons are found? const couponHashes = await PayloadBuilder.hashes(coupons) sources.push(...couponHashes) + const valuesAndConditions = [...payloads, ...conditions] const validCoupons = await this.filterToSigned( coupons .filter(this.isCouponCurrent) - .filter(coupon => areConditionsFulfilled(coupon, payloads)), + .filter(coupon => areConditionsFulfilled(coupon, valuesAndConditions)), ) // NOTE: Should we throw if not all coupons are valid? if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] @@ -146,10 +148,10 @@ export class PaymentDiscountDiviner< * @param hashMap The payloads to search for the discounts * @returns The discounts found in the payloads */ - protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record): Promise<[Coupon[]]> { + protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record): Promise<[Coupon[], Condition[]]> { // Parse discounts const hashes = terms.discounts ?? [] - if (hashes.length === 0) return [[]] + if (hashes.length === 0) return [[], []] // Use the supplied payloads to find the discounts const discounts: Coupon[] = hashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) @@ -172,7 +174,7 @@ export class PaymentDiscountDiviner< } // TODO: Find all non-supplied coupon conditions here as well - return [discounts] + return [discounts, []] } protected isCouponCurrent(coupon: Coupon): boolean { From a2e5b16936b73316b1eb88810c14a46f57189d93 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:31:34 -0500 Subject: [PATCH 4/7] Separate coupons and conditions in function signature --- .../Discount/lib/findUnfulfilledConditions.ts | 20 ++++++++++--------- .../spec/findUnfulfilledConditions.spec.ts | 20 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts b/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts index 6439f300a..b495119d2 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts @@ -1,8 +1,9 @@ import type { Hash } from '@xylabs/hex' import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { Payload, WithMeta } from '@xyo-network/payload-model' -import type { Coupon } from '@xyo-network/payment-payload-plugins' -import type { SchemaPayload } from '@xyo-network/schema-payload-plugin' +import type { Payload } from '@xyo-network/payload-model' +import { + type Condition, type Coupon, isCondition, +} from '@xyo-network/payment-payload-plugins' import { isSchemaPayloadWithMeta } from '@xyo-network/schema-payload-plugin' import type { ValidateFunction } from 'ajv' import { Ajv } from 'ajv' @@ -11,25 +12,26 @@ import { Ajv } from 'ajv' const ajv = new Ajv({ strict: false }) // Create the Ajv instance once const schemaCache = new Map() // Cache to store compiled validators -export const areConditionsFulfilled = async (coupon: Coupon, payloads: Payload[]): Promise => - (await findUnfulfilledConditions(coupon, payloads)).length === 0 +export const areConditionsFulfilled = async (coupon: Coupon, conditions: Condition[] = [], payloads: Payload[] = []): Promise => + (await findUnfulfilledConditions(coupon, conditions, payloads)).length === 0 // TODO: Should we separate conditions and payloads to prevent conflating data and "operands" (schemas to validate against data)? /** * Validates the conditions of a coupon against the provided payloads * @param coupon The coupon to check + * @param conditions The conditions associated with the coupon * @param payloads The associated payloads (containing the conditions and data to validate the conditions against) * @returns The unfulfilled condition hashes */ -export const findUnfulfilledConditions = async (coupon: Coupon, payloads: Payload[]): Promise => { +export const findUnfulfilledConditions = async (coupon: Coupon, conditions: Condition[] = [], payloads: Payload[] = []): Promise => { const unfulfilledConditions: Hash[] = [] // If there are no conditions, then they are fulfilled if (!coupon.conditions || coupon.conditions.length === 0) return unfulfilledConditions - const hashMap = await PayloadBuilder.toAllHashMap(payloads) + const hashMap = await PayloadBuilder.toAllHashMap([...conditions, ...payloads]) // Find all the conditions - const conditions = coupon.conditions.map(hash => hashMap[hash]).filter(isSchemaPayloadWithMeta) as WithMeta[] + const foundConditions = coupon.conditions.map(hash => hashMap[hash]).filter(isCondition) // Not all conditions were found - if (conditions.length !== coupon.conditions.length) { + if (foundConditions.length !== coupon.conditions.length) { const missing = coupon.conditions.filter(hash => !hashMap[hash]) unfulfilledConditions.push(...missing) return unfulfilledConditions diff --git a/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts b/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts index c2e215c3c..e652de3c2 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts @@ -130,7 +130,7 @@ describe('findUnfulfilledConditions', () => { const coupon: Coupon = { ...validCoupon, conditions } const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual([]) }) it('for multiple conditions', async () => { @@ -138,7 +138,7 @@ describe('findUnfulfilledConditions', () => { const coupon: Coupon = { ...validCoupon, conditions } const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] } const payloads = [terms, coupon, ...allConditions, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, allConditions, payloads) expect(results).toEqual([]) }) }) @@ -207,7 +207,7 @@ describe('findUnfulfilledConditions', () => { const conditions = [await PayloadBuilder.dataHash(rule)] const coupon: Coupon = { ...validCoupon, conditions } const payloads = [coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms assets do not exist', async () => { @@ -217,7 +217,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: undefined, } const payloads = [terms, coupon, rule, ...appraisals, ...assets] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms assets is empty', async () => { @@ -227,7 +227,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: [], } const payloads = [terms, coupon, rule, ...appraisals, ...assets] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms assets quantity does not exceed the required amount', async () => { @@ -238,7 +238,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: await PayloadBuilder.dataHashes(assets), } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) }) @@ -248,7 +248,7 @@ describe('findUnfulfilledConditions', () => { const conditions = [await PayloadBuilder.dataHash(rule)] const coupon: Coupon = { ...validCoupon, conditions } const payloads = [coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms buyer does not exist', async () => { @@ -258,7 +258,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], buyer: undefined, } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms buyers is empty', async () => { @@ -268,7 +268,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], buyer: [], } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms buyer does not contain specified address', async () => { @@ -285,7 +285,7 @@ describe('findUnfulfilledConditions', () => { buyer: [buyer.address], } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) }) From 3ff3d50c502afba419e9aa16a3a8b55f95cc10d1 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:34:34 -0500 Subject: [PATCH 5/7] Documentation and update tests --- .../src/Discount/lib/findUnfulfilledConditions.ts | 8 +++++++- .../lib/spec/findUnfulfilledConditions.spec.ts | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts b/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts index b495119d2..598d12188 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/findUnfulfilledConditions.ts @@ -12,10 +12,16 @@ import { Ajv } from 'ajv' const ajv = new Ajv({ strict: false }) // Create the Ajv instance once const schemaCache = new Map() // Cache to store compiled validators +/** + * Validates the conditions of a coupon against the provided payloads + * @param coupon The coupon to check + * @param conditions The conditions associated with the coupon + * @param payloads The associated payloads (containing the conditions and data to validate the conditions against) + * @returns True if all conditions are fulfilled, false otherwise + */ export const areConditionsFulfilled = async (coupon: Coupon, conditions: Condition[] = [], payloads: Payload[] = []): Promise => (await findUnfulfilledConditions(coupon, conditions, payloads)).length === 0 -// TODO: Should we separate conditions and payloads to prevent conflating data and "operands" (schemas to validate against data)? /** * Validates the conditions of a coupon against the provided payloads * @param coupon The coupon to check diff --git a/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts b/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts index e652de3c2..d4f16c637 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts @@ -151,7 +151,7 @@ describe('findUnfulfilledConditions', () => { const conditions = [await PayloadBuilder.dataHash(rule)] const coupon: Coupon = { ...validCoupon, conditions } const payloads = [coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms appraisals do not exist', async () => { @@ -161,7 +161,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], appraisals: undefined, } const payloads = [terms, coupon, rule, ...appraisals, ...assets] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when escrow terms appraisals is empty', async () => { @@ -171,7 +171,7 @@ describe('findUnfulfilledConditions', () => { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], appraisals: [], } const payloads = [terms, coupon, rule, ...appraisals, ...assets] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when appraisals not supplied', async () => { @@ -179,7 +179,7 @@ describe('findUnfulfilledConditions', () => { const coupon: Coupon = { ...validCoupon, conditions } const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] } const payloads = [terms, coupon, rule, ...assets] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) it('when supplied appraisal price exceeds the maximum amount', async () => { @@ -197,7 +197,7 @@ describe('findUnfulfilledConditions', () => { appraisals: await PayloadBuilder.dataHashes(appraisals), } const payloads = [terms, coupon, rule, ...assets, ...appraisals] - const results = await findUnfulfilledConditions(coupon, payloads) + const results = await findUnfulfilledConditions(coupon, [rule], payloads) expect(results).toEqual(conditions) }) }) From c94f942999b914071fbadc688a47b8d35d568017 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:38:26 -0500 Subject: [PATCH 6/7] Pass conditions separately to diviner --- packages/payloadset/packages/payments/src/Discount/Diviner.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts index 4712a4715..1749f323b 100644 --- a/packages/payloadset/packages/payments/src/Discount/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -77,11 +77,10 @@ export class PaymentDiscountDiviner< const couponHashes = await PayloadBuilder.hashes(coupons) sources.push(...couponHashes) - const valuesAndConditions = [...payloads, ...conditions] const validCoupons = await this.filterToSigned( coupons .filter(this.isCouponCurrent) - .filter(coupon => areConditionsFulfilled(coupon, valuesAndConditions)), + .filter(coupon => areConditionsFulfilled(coupon, conditions, payloads)), ) // NOTE: Should we throw if not all coupons are valid? if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] From 076403f26aa57053283d617a6cb85731a7eea11d Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Thu, 19 Sep 2024 08:51:09 -0500 Subject: [PATCH 7/7] Find conditions in addition to coupons --- .../packages/payments/src/Discount/Diviner.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts index 1749f323b..43dc390ce 100644 --- a/packages/payloadset/packages/payments/src/Discount/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -18,7 +18,7 @@ import { Condition, Coupon, Discount, - EscrowTerms, isCoupon, + EscrowTerms, isConditionWithMeta, isCoupon, isCouponWithMeta, isEscrowTerms, NO_DISCOUNT, PaymentDiscountDivinerConfigSchema, PaymentDiscountDivinerParams, } from '@xyo-network/payment-payload-plugins' @@ -145,35 +145,56 @@ export class PaymentDiscountDiviner< * Finds the discounts specified by the escrow terms from the supplied payloads * @param terms The escrow terms * @param hashMap The payloads to search for the discounts - * @returns The discounts found in the payloads + * @returns A tuple containing all the escrow coupons and conditions referenced in those coupons + * that were found in the either the supplied payloads or the archivist */ protected async getEscrowDiscounts(terms: EscrowTerms, hashMap: Record): Promise<[Coupon[], Condition[]]> { // Parse discounts - const hashes = terms.discounts ?? [] - if (hashes.length === 0) return [[], []] + const discountsHashes = terms.discounts ?? [] + if (discountsHashes.length === 0) return [[], []] // Use the supplied payloads to find the discounts - const discounts: Coupon[] = hashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) - const missing = hashes.filter(hash => !hashMap[hash]) + const discounts: Coupon[] = discountsHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) + const missingDiscounts = discountsHashes.filter(hash => !hashMap[hash]) // If not all discounts are found - if (missing.length > 0) { + if (missingDiscounts.length > 0) { // Find any remaining from discounts archivist const discountsArchivist = await this.getDiscountsArchivist() - const payloads = await discountsArchivist.get(missing) + const payloads = await discountsArchivist.get(missingDiscounts) discounts.push(...payloads.filter(isCouponWithMeta)) } // If not all discounts are found - if (discounts.length !== hashes.length) { + if (discounts.length !== discountsHashes.length) { const termsHash = await PayloadBuilder.hash(terms) const foundHashes = await PayloadBuilder.hashes(discounts) // Log individual discounts that were not found - for (const hash of hashes) { + for (const hash of discountsHashes) { if (!foundHashes.includes(hash)) console.warn(`Discount ${hash} not found for terms ${termsHash}`) } } - // TODO: Find all non-supplied coupon conditions here as well - return [discounts, []] + const conditionsHashes: Hash[] = discounts.flatMap(discount => discount.conditions ?? []) + const conditions: Condition[] = conditionsHashes.map(hash => hashMap[hash]).filter(exists).filter(isConditionWithMeta) + const missingConditions = conditionsHashes.filter(hash => !hashMap[hash]) + + // If not all conditions are found + if (missingConditions.length > 0) { + // Find any remaining from discounts archivist + const discountsArchivist = await this.getDiscountsArchivist() + const payloads = await discountsArchivist.get(missingConditions) + conditions.push(...payloads.filter(isConditionWithMeta)) + } + // If not all conditions are found + if (conditions.length !== conditionsHashes.length) { + const termsHash = await PayloadBuilder.hash(terms) + const foundHashes = await PayloadBuilder.hashes(conditions) + // Log individual conditions that were not found + for (const hash of discountsHashes) { + if (!foundHashes.includes(hash)) console.warn(`Coupon condition ${hash} not found for terms ${termsHash}`) + } + } + + return [discounts, conditions] } protected isCouponCurrent(coupon: Coupon): boolean {