@xyo-network/payment-plugin 4.1.1 → 5.0.0

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.
@@ -1,88 +1,5 @@
1
- import { Address, Hash } from '@xylabs/hex';
2
- import { ArchivistInstance } from '@xyo-network/archivist-model';
3
- import { AbstractDiviner } from '@xyo-network/diviner-abstract';
4
- import { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease';
5
- import { DivinerModuleEventData, DivinerInstance } from '@xyo-network/diviner-model';
6
- import { Payload } from '@xyo-network/payload-model';
7
- import { EscrowTerms, Coupon, PaymentDiscountDivinerParams, Discount, Condition, Invoice, PaymentSubtotalDivinerParams, Subtotal, PaymentTotalDivinerParams, Total, PaymentTotalDivinerConfigSchema } from '@xyo-network/payment-payload-plugins';
8
-
9
- type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload;
10
- declare class PaymentDiscountDiviner<TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams, TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType, TOut extends Discount = Discount, TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut>> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {
11
- static readonly configSchemas: string[];
12
- static readonly defaultConfigSchema = "network.xyo.diviner.payments.discount.config";
13
- protected get couponAuthorities(): Address[];
14
- protected divineHandler(payloads?: TIn[]): Promise<TOut[]>;
15
- /**
16
- * Filters the supplied list of coupons to only those that are signed by
17
- * addresses specified in the couponAuthorities
18
- * @param coupons The list of coupons to filter
19
- * @returns The filtered list of coupons that are signed by the couponAuthorities
20
- */
21
- protected filterToSigned(coupons: Coupon[]): Promise<Coupon[]>;
22
- protected getDiscountsArchivist(): Promise<ArchivistInstance>;
23
- protected getDiscountsBoundWitnessDiviner(): Promise<DivinerInstance>;
24
- /**
25
- * Finds the appraisals specified by the escrow terms from the supplied payloads
26
- * @param terms The escrow terms
27
- * @param payloads The payloads to search for the appraisals
28
- * @returns The appraisals found in the payloads
29
- */
30
- protected getEscrowAppraisals(terms: EscrowTerms, hashMap: Record<Hash, Payload>): HashLeaseEstimate[];
31
- /**
32
- * Finds the discounts specified by the escrow terms from the supplied payloads
33
- * @param terms The escrow terms
34
- * @param hashMap The payloads to search for the discounts
35
- * @returns A tuple containing all the escrow coupons and conditions referenced in those coupons
36
- * that were found in the either the supplied payloads or the archivist
37
- */
38
- protected getEscrowDiscounts(terms: EscrowTerms, hashMap: Record<Hash, Payload>): Promise<[Coupon[], Condition[]]>;
39
- protected isCouponCurrent(coupon: Coupon): boolean;
40
- }
41
-
42
- declare const applyCoupons: (appraisals: HashLeaseEstimate[], coupons: Coupon[]) => Discount;
43
-
44
- /**
45
- * Validates the conditions of a coupon against the provided payloads
46
- * @param coupon The coupon to check
47
- * @param conditions The conditions associated with the coupon
48
- * @param payloads The associated payloads (containing the conditions and data to validate the conditions against)
49
- * @returns True if all conditions are fulfilled, false otherwise
50
- */
51
- declare const areConditionsFulfilled: (coupon: Coupon, conditions?: Condition[], payloads?: Payload[]) => Promise<boolean>;
52
- /**
53
- * Validates the conditions of a coupon against the provided payloads
54
- * @param coupon The coupon to check
55
- * @param conditions The conditions associated with the coupon
56
- * @param payloads The associated payloads (containing the conditions and data to validate the conditions against)
57
- * @returns The unfulfilled condition hashes
58
- */
59
- declare const findUnfulfilledConditions: (coupon: Coupon, conditions?: Condition[], payloads?: Payload[]) => Promise<Hash[]>;
60
-
61
- /**
62
- * Validates the escrow terms to ensure they are valid for a purchase
63
- * @returns A payment if the terms are valid for a purchase, undefined otherwise
64
- */
65
- declare const getInvoiceForEscrow: (terms: EscrowTerms, dataHashMap: Record<Hash, Payload>, paymentTotalDiviner: DivinerInstance) => Promise<Invoice | undefined>;
66
-
67
- /**
68
- * Escrow terms that contain all the valid fields for calculating a subtotal
69
- */
70
- type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload;
71
- declare class PaymentSubtotalDiviner<TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams, TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType, TOut extends Subtotal = Subtotal, TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut>> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {
72
- static readonly configSchemas: string[];
73
- static readonly defaultConfigSchema = "network.xyo.diviner.payments.subtotal.config";
74
- protected divineHandler(payloads?: TIn[]): Promise<TOut[]>;
75
- }
76
-
77
- type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType;
78
- type OutputType = Subtotal | Discount | Total;
79
- declare class PaymentTotalDiviner<TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams, TIn extends InputType = InputType, TOut extends OutputType = OutputType, TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut>> extends AbstractDiviner<TParams, TIn, TOut, TEventData> {
80
- static readonly configSchemas: string[];
81
- static readonly defaultConfigSchema: PaymentTotalDivinerConfigSchema;
82
- protected divineHandler(payloads?: TIn[]): Promise<TOut[]>;
83
- protected getPaymentDiscountsDiviner(): Promise<PaymentDiscountDiviner>;
84
- protected getPaymentSubtotalDiviner(): Promise<PaymentSubtotalDiviner>;
85
- }
86
-
87
- export { PaymentDiscountDiviner, PaymentSubtotalDiviner, PaymentTotalDiviner, applyCoupons, areConditionsFulfilled, findUnfulfilledConditions, getInvoiceForEscrow };
88
- export type { PaymentDiscountDivinerInputType, PaymentSubtotalDivinerInputType };
1
+ export * from './Discount/index.ts';
2
+ export * from './Invoice/index.ts';
3
+ export * from './Subtotal/index.ts';
4
+ export * from './Total/index.ts';
5
+ //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyo-network/payment-plugin",
3
- "version": "4.1.1",
3
+ "version": "5.0.0",
4
4
  "description": "Typescript/Javascript Plugins for XYO Platform",
5
5
  "homepage": "https://xyo.network",
6
6
  "bugs": {
@@ -28,36 +28,39 @@
28
28
  },
29
29
  "module": "dist/neutral/index.mjs",
30
30
  "types": "dist/neutral/index.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "src"
34
+ ],
31
35
  "dependencies": {
32
- "@xylabs/array": "^4.13.23",
33
- "@xylabs/assert": "^4.13.23",
34
- "@xylabs/exists": "^4.13.23",
35
- "@xylabs/hex": "^4.13.23",
36
- "@xyo-network/archivist-model": "^4.1.7",
37
- "@xyo-network/diviner-abstract": "^4.1.7",
38
- "@xyo-network/diviner-boundwitness-model": "^4.1.7",
39
- "@xyo-network/diviner-hash-lease": "^4.1.7",
40
- "@xyo-network/diviner-model": "^4.1.7",
41
- "@xyo-network/module-model": "^4.1.7",
42
- "@xyo-network/payload-builder": "^4.1.7",
43
- "@xyo-network/payload-model": "^4.1.7",
44
- "@xyo-network/payment-payload-plugins": "^4.1.1",
45
- "@xyo-network/schema-payload-plugin": "^4.1.7",
46
- "@xyo-network/xns-record-payload-plugins": "^4.1.1",
36
+ "@xylabs/array": "^5.0.0",
37
+ "@xylabs/assert": "^5.0.0",
38
+ "@xylabs/exists": "^5.0.0",
39
+ "@xylabs/hex": "^5.0.0",
40
+ "@xyo-network/archivist-model": "^5.0.0",
41
+ "@xyo-network/diviner-abstract": "^5.0.0",
42
+ "@xyo-network/diviner-boundwitness-model": "^5.0.0",
43
+ "@xyo-network/diviner-hash-lease": "^5.0.0",
44
+ "@xyo-network/diviner-model": "^5.0.0",
45
+ "@xyo-network/module-model": "^5.0.0",
46
+ "@xyo-network/payload-builder": "^5.0.0",
47
+ "@xyo-network/payload-model": "^5.0.0",
48
+ "@xyo-network/payment-payload-plugins": "^5.0.0",
49
+ "@xyo-network/schema-payload-plugin": "^5.0.0",
50
+ "@xyo-network/xns-record-payload-plugins": "^5.0.0",
47
51
  "ajv": "^8.17.1"
48
52
  },
49
53
  "devDependencies": {
50
- "@xylabs/ts-scripts-yarn3": "^7.0.0-rc.24",
51
- "@xylabs/tsconfig": "^7.0.0-rc.24",
52
- "@xylabs/vitest-extended": "^4.13.23",
53
- "@xyo-network/archivist-memory": "^4.1.7",
54
- "@xyo-network/boundwitness-builder": "^4.1.7",
55
- "@xyo-network/diviner-boundwitness-memory": "^4.1.7",
56
- "@xyo-network/id-payload-plugin": "^4.1.7",
57
- "@xyo-network/node-memory": "^4.1.7",
58
- "@xyo-network/wallet": "^4.1.7",
59
- "@xyo-network/wallet-model": "^4.1.7",
60
- "knip": "^5.62.0",
54
+ "@xylabs/ts-scripts-yarn3": "^7.0.2",
55
+ "@xylabs/tsconfig": "^7.0.2",
56
+ "@xylabs/vitest-extended": "^5.0.0",
57
+ "@xyo-network/archivist-memory": "^5.0.0",
58
+ "@xyo-network/boundwitness-builder": "^5.0.0",
59
+ "@xyo-network/diviner-boundwitness-memory": "^5.0.0",
60
+ "@xyo-network/id-payload-plugin": "^5.0.0",
61
+ "@xyo-network/node-memory": "^5.0.0",
62
+ "@xyo-network/wallet": "^5.0.0",
63
+ "@xyo-network/wallet-model": "^5.0.0",
61
64
  "typescript": "^5.8.3",
62
65
  "vitest": "^3.2.4"
63
66
  },
@@ -0,0 +1,104 @@
1
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
2
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
3
+ import type { Coupon } from '@xyo-network/payment-payload-plugins'
4
+ import {
5
+ DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema,
6
+ FixedPriceCouponSchema,
7
+ } from '@xyo-network/payment-payload-plugins'
8
+ import {
9
+ beforeEach, describe, expect,
10
+ it, vi,
11
+ } from 'vitest'
12
+
13
+ import { applyCoupons } from '../applyCoupons.ts'
14
+
15
+ describe('applyCoupons', () => {
16
+ const nbf = Date.now()
17
+ const exp = Date.now() + 10_000_000
18
+ // Coupons
19
+ const TEN_DOLLAR_OFF_COUPON: Coupon = {
20
+ amount: 10, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD',
21
+ }
22
+ const TEN_PERCENT_OFF_COUPON: Coupon = {
23
+ percentage: 0.1, exp, nbf, schema: FixedPercentageCouponSchema,
24
+ }
25
+ const TEN_DOLLAR_ITEM_COUPON: Coupon = {
26
+ amount: 10, exp, nbf, schema: FixedPriceCouponSchema, currency: 'USD',
27
+ }
28
+
29
+ // Appraisals
30
+ const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = {
31
+ price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema,
32
+ }
33
+ const SEVENTY_DOLLAR_ESTIMATE: HashLeaseEstimate = {
34
+ price: 70, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema,
35
+ }
36
+ const THIRTY_DOLLAR_ESTIMATE: HashLeaseEstimate = {
37
+ price: 30, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema,
38
+ }
39
+
40
+ beforeEach(() => {
41
+ vi.clearAllMocks()
42
+ })
43
+ describe('when coupon is less than total', () => {
44
+ const validCoupons: [estimates: HashLeaseEstimate[], coupons: Coupon[], discount: number][] = [
45
+ // $100 item with $10 off coupon = $90 total/$10 discount
46
+ [[HUNDRED_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON], 10],
47
+ // Two items (totalling $100) with $10 off coupon = $90 total/$10 discount
48
+ [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON], 10],
49
+ // $100 item with 10% off coupon = $90 total/$10 discount
50
+ [[HUNDRED_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON], 10],
51
+ // Two items (totalling $100) with 10% off coupon = $90 total/$10 discount
52
+ [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON], 10],
53
+ // Two items (totalling $100) with 10% off coupon = $90 total/$10 discount
54
+ [[THIRTY_DOLLAR_ESTIMATE, SEVENTY_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON], 10],
55
+ // $100 item with $10 per item coupon = $10 total/$90 discount
56
+ [[HUNDRED_DOLLAR_ESTIMATE], [TEN_DOLLAR_ITEM_COUPON], 90],
57
+ // Two items (totalling $100) with $10 per item coupon = $20 total/$80 discount
58
+ [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_ITEM_COUPON], 80],
59
+ // Four items (totalling $200) with $10 per item coupon = $40 total/$160 discount
60
+ [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE, SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_ITEM_COUPON], 160],
61
+ ]
62
+ it.each(validCoupons)('Applies coupon discount', (estimates, coupons, amount) => {
63
+ const results = applyCoupons(estimates, coupons)
64
+ expect(results).toEqual({
65
+ amount, schema: DiscountSchema, currency: 'USD',
66
+ })
67
+ })
68
+ })
69
+ describe('when discount exceeds total', () => {
70
+ const discountExceedsTotal: [HashLeaseEstimate[], Coupon[]][] = [
71
+ [
72
+ [HUNDRED_DOLLAR_ESTIMATE],
73
+ [{
74
+ amount: 101, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD',
75
+ }]],
76
+ [
77
+ [HUNDRED_DOLLAR_ESTIMATE],
78
+ [{
79
+ percentage: 1.1, exp, nbf, schema: FixedPercentageCouponSchema,
80
+ }]],
81
+ ]
82
+ it.each(discountExceedsTotal)('Discounts only to total', (estimates, coupons) => {
83
+ const results = applyCoupons(estimates, coupons)
84
+ const amount = estimates.reduce((acc, a) => acc + a.price, 0)
85
+ expect(results).toEqual({
86
+ amount, schema: DiscountSchema, currency: 'USD',
87
+ })
88
+ })
89
+ })
90
+ describe('with stackable discounts', () => {
91
+ const STACKABLE_TEN_DOLLAR_OFF_COUPON: Coupon = { ...TEN_DOLLAR_OFF_COUPON, stackable: true }
92
+ const STACKABLE_TEN_PERCENT_OFF_COUPON: Coupon = { ...TEN_PERCENT_OFF_COUPON, stackable: true }
93
+ const validCoupons: [HashLeaseEstimate[], Coupon[], number][] = [
94
+ [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_DOLLAR_OFF_COUPON, STACKABLE_TEN_PERCENT_OFF_COUPON], 19],
95
+ [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_PERCENT_OFF_COUPON, STACKABLE_TEN_DOLLAR_OFF_COUPON], 19],
96
+ ]
97
+ it.each(validCoupons)('Applies both discounts', (estimates, coupons, amount) => {
98
+ const results = applyCoupons(estimates, coupons)
99
+ expect(results).toEqual({
100
+ amount, schema: DiscountSchema, currency: 'USD',
101
+ })
102
+ })
103
+ })
104
+ })
@@ -0,0 +1,243 @@
1
+ import type { Address } from '@xylabs/hex'
2
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
3
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
4
+ import type { Id } from '@xyo-network/id-payload-plugin'
5
+ import { IdSchema } from '@xyo-network/id-payload-plugin'
6
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
7
+ import type {
8
+ BuyerCondition,
9
+ Condition, Coupon, EscrowTerms,
10
+ } from '@xyo-network/payment-payload-plugins'
11
+ import {
12
+ createConditionForMaximumAppraisalAmount,
13
+ createConditionForMinimumAssetQuantity, createConditionForRequiredBuyer,
14
+ EscrowTermsSchema, FixedAmountCouponSchema,
15
+ } from '@xyo-network/payment-payload-plugins'
16
+ import type { SchemaPayload } from '@xyo-network/schema-payload-plugin'
17
+ import { HDWallet } from '@xyo-network/wallet'
18
+ import type { WalletInstance } from '@xyo-network/wallet-model'
19
+ import {
20
+ beforeEach, describe, expect,
21
+ it,
22
+ } from 'vitest'
23
+
24
+ import { findUnfulfilledConditions } from '../findUnfulfilledConditions.ts'
25
+
26
+ describe('findUnfulfilledConditions', () => {
27
+ const nbf = Date.now()
28
+ const exp = Date.now() + 10_000_000
29
+ // Coupons
30
+ const validCoupon: Coupon = {
31
+ amount: 10, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD',
32
+ }
33
+
34
+ // Conditions
35
+ const CONDITION_REQUIRES_BUYING_TWO: Condition = createConditionForMinimumAssetQuantity(2)
36
+ const CONDITION_REQUIRES_APPRAISAL_DOES_NOT_EXCEED_AMOUNT: Condition = createConditionForMaximumAppraisalAmount(20)
37
+ const CONDITION_FOR_SPECIFIC_BUYER: BuyerCondition = createConditionForRequiredBuyer('TODO: Replace in beforeAll' as Address)
38
+
39
+ const allConditions: SchemaPayload[] = [
40
+ CONDITION_REQUIRES_BUYING_TWO,
41
+ CONDITION_REQUIRES_APPRAISAL_DOES_NOT_EXCEED_AMOUNT,
42
+ CONDITION_FOR_SPECIFIC_BUYER,
43
+ ]
44
+
45
+ let buyer: WalletInstance
46
+ let seller: WalletInstance
47
+ const baseTerms: EscrowTerms = {
48
+ schema: EscrowTermsSchema, appraisals: [], exp, nbf,
49
+ }
50
+ const appraisal1: HashLeaseEstimate = {
51
+ schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf,
52
+ }
53
+ const appraisal2: HashLeaseEstimate = {
54
+ schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf,
55
+ }
56
+ const appraisals = [appraisal1, appraisal2]
57
+
58
+ const asset1: Id = { schema: IdSchema, salt: nbf.toString() }
59
+ const asset2: Id = { schema: IdSchema, salt: exp.toString() }
60
+ const assets = [asset1, asset2]
61
+
62
+ beforeEach(async () => {
63
+ buyer = await HDWallet.random()
64
+ seller = await HDWallet.random()
65
+
66
+ // Configure base terms
67
+ baseTerms.buyer = [buyer.address]
68
+ baseTerms.seller = [seller.address]
69
+ baseTerms.appraisals = await PayloadBuilder.dataHashes(appraisals)
70
+ baseTerms.assets = await PayloadBuilder.dataHashes(assets)
71
+
72
+ // Configure condition for specific buyer
73
+ CONDITION_FOR_SPECIFIC_BUYER.definition.contains.properties.buyer.items.const = buyer.address
74
+ })
75
+ describe('when all conditions are fulfilled', () => {
76
+ describe('returns empty array', () => {
77
+ it.each(allConditions)('for single condition', async (rule) => {
78
+ const conditions = [await PayloadBuilder.dataHash(rule)]
79
+ const coupon: Coupon = { ...validCoupon, conditions }
80
+ const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] }
81
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
82
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
83
+ expect(results).toEqual([])
84
+ })
85
+ it('for multiple conditions', async () => {
86
+ const conditions = await PayloadBuilder.dataHashes(allConditions)
87
+ const coupon: Coupon = { ...validCoupon, conditions }
88
+ const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] }
89
+ const payloads = [terms, coupon, ...allConditions, ...assets, ...appraisals]
90
+ const results = await findUnfulfilledConditions(coupon, allConditions, payloads)
91
+ expect(results).toEqual([])
92
+ })
93
+ })
94
+ })
95
+ describe('when conditions are not fulfilled', () => {
96
+ describe('returns all unfulfilled condition hashes', () => {
97
+ describe('for maximum appraisal amount', () => {
98
+ const rule = CONDITION_REQUIRES_APPRAISAL_DOES_NOT_EXCEED_AMOUNT
99
+ it('when escrow terms do not exist', async () => {
100
+ const conditions = [await PayloadBuilder.dataHash(rule)]
101
+ const coupon: Coupon = { ...validCoupon, conditions }
102
+ const payloads = [coupon, rule, ...assets, ...appraisals]
103
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
104
+ expect(results).toEqual(conditions)
105
+ })
106
+ it('when escrow terms appraisals do not exist', async () => {
107
+ const conditions = [await PayloadBuilder.dataHash(rule)]
108
+ const coupon: Coupon = { ...validCoupon, conditions }
109
+ const terms: EscrowTerms = {
110
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], appraisals: undefined,
111
+ }
112
+ const payloads = [terms, coupon, rule, ...appraisals, ...assets]
113
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
114
+ expect(results).toEqual(conditions)
115
+ })
116
+ it('when escrow terms appraisals is empty', async () => {
117
+ const conditions = [await PayloadBuilder.dataHash(rule)]
118
+ const coupon: Coupon = { ...validCoupon, conditions }
119
+ const terms: EscrowTerms = {
120
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], appraisals: [],
121
+ }
122
+ const payloads = [terms, coupon, rule, ...appraisals, ...assets]
123
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
124
+ expect(results).toEqual(conditions)
125
+ })
126
+ it('when appraisals not supplied', async () => {
127
+ const conditions = [await PayloadBuilder.dataHash(rule)]
128
+ const coupon: Coupon = { ...validCoupon, conditions }
129
+ const terms: EscrowTerms = { ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)] }
130
+ const payloads = [terms, coupon, rule, ...assets]
131
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
132
+ expect(results).toEqual(conditions)
133
+ })
134
+ it('when supplied appraisal price exceeds the maximum amount', async () => {
135
+ const conditions = [await PayloadBuilder.dataHash(rule)]
136
+ const coupon: Coupon = { ...validCoupon, conditions }
137
+ const assets = [asset1]
138
+ const appraisal1: HashLeaseEstimate = {
139
+ schema: HashLeaseEstimateSchema, price: 1000, currency: 'USD', exp, nbf,
140
+ }
141
+ const appraisals = [appraisal1]
142
+ const terms: EscrowTerms = {
143
+ ...baseTerms,
144
+ discounts: [await PayloadBuilder.dataHash(coupon)],
145
+ assets: await PayloadBuilder.dataHashes(assets),
146
+ appraisals: await PayloadBuilder.dataHashes(appraisals),
147
+ }
148
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
149
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
150
+ expect(results).toEqual(conditions)
151
+ })
152
+ })
153
+ describe('for minimum purchase quantity', () => {
154
+ const rule = CONDITION_REQUIRES_BUYING_TWO
155
+ it('when escrow terms do not exist', async () => {
156
+ const conditions = [await PayloadBuilder.dataHash(rule)]
157
+ const coupon: Coupon = { ...validCoupon, conditions }
158
+ const payloads = [coupon, rule, ...assets, ...appraisals]
159
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
160
+ expect(results).toEqual(conditions)
161
+ })
162
+ it('when escrow terms assets do not exist', async () => {
163
+ const conditions = [await PayloadBuilder.dataHash(rule)]
164
+ const coupon: Coupon = { ...validCoupon, conditions }
165
+ const terms: EscrowTerms = {
166
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: undefined,
167
+ }
168
+ const payloads = [terms, coupon, rule, ...appraisals, ...assets]
169
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
170
+ expect(results).toEqual(conditions)
171
+ })
172
+ it('when escrow terms assets is empty', async () => {
173
+ const conditions = [await PayloadBuilder.dataHash(rule)]
174
+ const coupon: Coupon = { ...validCoupon, conditions }
175
+ const terms: EscrowTerms = {
176
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: [],
177
+ }
178
+ const payloads = [terms, coupon, rule, ...appraisals, ...assets]
179
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
180
+ expect(results).toEqual(conditions)
181
+ })
182
+ it('when escrow terms assets quantity does not exceed the required amount', async () => {
183
+ const conditions = [await PayloadBuilder.dataHash(rule)]
184
+ const coupon: Coupon = { ...validCoupon, conditions }
185
+ const assets = [asset1]
186
+ const terms: EscrowTerms = {
187
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], assets: await PayloadBuilder.dataHashes(assets),
188
+ }
189
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
190
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
191
+ expect(results).toEqual(conditions)
192
+ })
193
+ })
194
+ describe('for specific buyer', () => {
195
+ const rule = CONDITION_FOR_SPECIFIC_BUYER
196
+ it('when escrow terms do not exist', async () => {
197
+ const conditions = [await PayloadBuilder.dataHash(rule)]
198
+ const coupon: Coupon = { ...validCoupon, conditions }
199
+ const payloads = [coupon, rule, ...assets, ...appraisals]
200
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
201
+ expect(results).toEqual(conditions)
202
+ })
203
+ it('when escrow terms buyer does not exist', async () => {
204
+ const conditions = [await PayloadBuilder.dataHash(rule)]
205
+ const coupon: Coupon = { ...validCoupon, conditions }
206
+ const terms: EscrowTerms = {
207
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], buyer: undefined,
208
+ }
209
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
210
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
211
+ expect(results).toEqual(conditions)
212
+ })
213
+ it('when escrow terms buyers is empty', async () => {
214
+ const conditions = [await PayloadBuilder.dataHash(rule)]
215
+ const coupon: Coupon = { ...validCoupon, conditions }
216
+ const terms: EscrowTerms = {
217
+ ...baseTerms, discounts: [await PayloadBuilder.dataHash(coupon)], buyer: [],
218
+ }
219
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
220
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
221
+ expect(results).toEqual(conditions)
222
+ })
223
+ it('when escrow terms buyer does not contain specified address', async () => {
224
+ const buyer = await HDWallet.random()
225
+ const conditions = [await PayloadBuilder.dataHash(rule)]
226
+ const coupon: Coupon = { ...validCoupon, conditions }
227
+ const assets = [asset1]
228
+ const appraisals = [appraisal1]
229
+ const terms: EscrowTerms = {
230
+ ...baseTerms,
231
+ discounts: [await PayloadBuilder.dataHash(coupon)],
232
+ assets: await PayloadBuilder.dataHashes(assets),
233
+ appraisals: await PayloadBuilder.dataHashes(appraisals),
234
+ buyer: [buyer.address],
235
+ }
236
+ const payloads = [terms, coupon, rule, ...assets, ...appraisals]
237
+ const results = await findUnfulfilledConditions(coupon, [rule], payloads)
238
+ expect(results).toEqual(conditions)
239
+ })
240
+ })
241
+ })
242
+ })
243
+ })
@@ -0,0 +1,128 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { MemoryArchivist } from '@xyo-network/archivist-memory'
4
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
5
+ import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory'
6
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
7
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
8
+ import { MemoryNode } from '@xyo-network/node-memory'
9
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
10
+ import type { Coupon, EscrowTerms } from '@xyo-network/payment-payload-plugins'
11
+ import {
12
+ DiscountSchema,
13
+ EscrowTermsSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema,
14
+ isDiscount,
15
+ PaymentDiscountDivinerConfigSchema,
16
+ } from '@xyo-network/payment-payload-plugins'
17
+ import { HDWallet } from '@xyo-network/wallet'
18
+ import {
19
+ beforeAll, beforeEach, describe, expect, it, vi,
20
+ } from 'vitest'
21
+
22
+ import { PaymentDiscountDiviner } from '../Diviner.ts'
23
+
24
+ describe('PaymentDiscountDiviner', () => {
25
+ let sut: PaymentDiscountDiviner
26
+ const nbf = Date.now()
27
+ const exp = Number.MAX_SAFE_INTEGER
28
+ const termsBase: EscrowTerms = {
29
+ schema: EscrowTermsSchema, appraisals: [], exp, nbf,
30
+ }
31
+ const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = {
32
+ price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema,
33
+ }
34
+ const validCoupons: Coupon[] = [
35
+ {
36
+ amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD',
37
+ },
38
+ {
39
+ percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema,
40
+ },
41
+ ]
42
+ const unsignedCoupons: Coupon[] = [
43
+ {
44
+ amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD',
45
+ },
46
+ {
47
+ percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema,
48
+ },
49
+ ]
50
+ beforeAll(async () => {
51
+ termsBase.appraisals?.push(await PayloadBuilder.hash(HUNDRED_DOLLAR_ESTIMATE))
52
+ const node = await MemoryNode.create({ account: 'random' })
53
+ const archivist = await MemoryArchivist.create({ account: 'random' })
54
+ const signer = await HDWallet.random()
55
+ // Sign the valid coupons and insert them into the archivist
56
+ for (const coupon of validCoupons) {
57
+ const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build()
58
+ await archivist.insert([bw, ...payloads])
59
+ }
60
+ // Insert (but do not sign) the unsigned coupons into the archivist
61
+ await archivist.insert(unsignedCoupons)
62
+ const boundWitnessDiviner = await MemoryBoundWitnessDiviner.create({
63
+ account: 'random',
64
+ config: {
65
+ archivist: archivist.address,
66
+ schema: MemoryBoundWitnessDiviner.defaultConfigSchema,
67
+ },
68
+ })
69
+ sut = await PaymentDiscountDiviner.create({
70
+ account: 'random',
71
+ config: {
72
+ archivist: archivist.address,
73
+ boundWitnessDiviner: boundWitnessDiviner.address,
74
+ couponAuthorities: [signer.address],
75
+ schema: PaymentDiscountDivinerConfigSchema,
76
+ },
77
+ })
78
+ const modules = [archivist, boundWitnessDiviner, sut]
79
+ for (const mod of modules) {
80
+ await node.register(mod)
81
+ await node.attach(mod.address, false)
82
+ }
83
+ })
84
+ beforeEach(() => {
85
+ vi.clearAllMocks()
86
+ })
87
+ describe('with valid coupon', () => {
88
+ it.each(validCoupons)('Applies coupon', async (coupon) => {
89
+ const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] }
90
+ const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon])
91
+ expect(results).toBeArrayOfSize(1)
92
+ const result = results.find(isDiscount)
93
+ expect(result).toBeDefined()
94
+ expect(result).toMatchObject({ amount: 10, schema: DiscountSchema })
95
+ })
96
+ })
97
+ describe('with invalid coupon', () => {
98
+ it.each(unsignedCoupons)('Does not apply coupons', async (coupon) => {
99
+ const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] }
100
+ const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon])
101
+ expect(results).toBeArrayOfSize(1)
102
+ const result = results.find(isDiscount)
103
+ expect(result).toBeDefined()
104
+ expect(result).toMatchObject({ amount: 0, schema: DiscountSchema })
105
+ })
106
+ })
107
+ describe('with expired coupon', () => {
108
+ const now = Date.now()
109
+ const expiredCoupons: Coupon[] = [
110
+ // In past
111
+ {
112
+ amount: 10, exp: now, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD',
113
+ },
114
+ // In future
115
+ {
116
+ percentage: 0.1, exp: now + 100_000_000, nbf: now + 10_000_000, schema: FixedPercentageCouponSchema,
117
+ },
118
+ ]
119
+ it.each(expiredCoupons)('Does not apply coupons', async (coupon) => {
120
+ const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] }
121
+ const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon])
122
+ expect(results).toBeArrayOfSize(1)
123
+ const result = results.find(isDiscount)
124
+ expect(result).toBeDefined()
125
+ expect(result).toMatchObject({ amount: 0, schema: DiscountSchema })
126
+ })
127
+ })
128
+ })
@@ -0,0 +1,94 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { assertEx } from '@xylabs/assert'
4
+ import { MemoryArchivist } from '@xyo-network/archivist-memory'
5
+ import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory'
6
+ import { BoundWitnessDivinerConfigSchema } from '@xyo-network/diviner-boundwitness-model'
7
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
8
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
9
+ import { MemoryNode } from '@xyo-network/node-memory'
10
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
11
+ import type { EscrowTerms } from '@xyo-network/payment-payload-plugins'
12
+ import { EscrowTermsSchema, NO_DISCOUNT } from '@xyo-network/payment-payload-plugins'
13
+ import {
14
+ beforeAll,
15
+ describe, expect, it,
16
+ } from 'vitest'
17
+
18
+ import { PaymentDiscountDiviner } from '../../Discount/index.ts'
19
+ import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts'
20
+ import { PaymentTotalDiviner } from '../../Total/index.ts'
21
+ import { getInvoiceForEscrow } from '../getInvoiceForEscrow.ts'
22
+
23
+ describe('getInvoiceForEscrow', () => {
24
+ let node: MemoryNode
25
+ let paymentTotalDiviner: PaymentTotalDiviner
26
+ beforeAll(async () => {
27
+ node = await MemoryNode.create({ account: 'random' })
28
+ const discountsArchivist = await MemoryArchivist.create({ account: 'random' })
29
+ const discountsBoundWitnessDiviner = await MemoryBoundWitnessDiviner.create({
30
+ account: 'random',
31
+ config: { archivist: discountsArchivist.address, schema: BoundWitnessDivinerConfigSchema },
32
+ })
33
+ const paymentDiscountDiviner = await PaymentDiscountDiviner.create({
34
+ account: 'random',
35
+ config: {
36
+ archivist: discountsArchivist.address,
37
+ boundWitnessDiviner: discountsBoundWitnessDiviner.address,
38
+ schema: PaymentDiscountDiviner.defaultConfigSchema,
39
+ },
40
+ })
41
+ const paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' })
42
+ paymentTotalDiviner = await PaymentTotalDiviner.create({
43
+ account: 'random',
44
+ config: {
45
+ paymentDiscountDiviner: paymentDiscountDiviner.address,
46
+ paymentSubtotalDiviner: paymentSubtotalDiviner.address,
47
+ schema: PaymentTotalDiviner.defaultConfigSchema,
48
+ },
49
+ })
50
+ const modules = [
51
+ discountsArchivist,
52
+ discountsBoundWitnessDiviner,
53
+ paymentDiscountDiviner,
54
+ paymentSubtotalDiviner,
55
+ paymentTotalDiviner]
56
+ for (const module of modules) {
57
+ await node.register(module)
58
+ await node.attach(module.address, true)
59
+ }
60
+ })
61
+ describe('with no discount', () => {
62
+ it('should return invoice values', async () => {
63
+ const nbf = Date.now()
64
+ const exp = nbf + 1000 * 60 * 10
65
+ const appraisal: HashLeaseEstimate = {
66
+ price: 10, currency: 'USD', schema: HashLeaseEstimateSchema, exp, nbf,
67
+ }
68
+ const appraisalHash = await PayloadBuilder.dataHash(appraisal)
69
+ const terms: EscrowTerms = {
70
+ schema: EscrowTermsSchema, appraisals: [appraisalHash], exp, nbf,
71
+ }
72
+ const dataHashMap = await PayloadBuilder.toDataHashMap([appraisal])
73
+ const result = await getInvoiceForEscrow(terms, dataHashMap, paymentTotalDiviner)
74
+ expect(result).toBeArray()
75
+ expect(result?.length).toBeGreaterThan(0)
76
+ const invoice = assertEx(result)
77
+ const [subtotal, total, payment, discount] = invoice
78
+ expect(subtotal).toBeDefined()
79
+ expect(subtotal.amount).toBeNumber()
80
+ expect(subtotal.amount).toBe(appraisal.price)
81
+ expect(subtotal.currency).toBe('USD')
82
+ expect(total).toBeDefined()
83
+ expect(total.amount).toBeNumber()
84
+ expect(total.amount).toBe(subtotal.amount)
85
+ expect(total.currency).toBe('USD')
86
+ expect(payment).toBeDefined()
87
+ expect(payment.amount).toBeNumber()
88
+ expect(payment.amount).toBe(total.amount)
89
+ expect(payment.currency).toBe('USD')
90
+ expect(discount).toBeDefined()
91
+ expect(discount).toMatchObject(NO_DISCOUNT)
92
+ })
93
+ })
94
+ })
@@ -0,0 +1,113 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
4
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
5
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
6
+ import type { EscrowTerms } from '@xyo-network/payment-payload-plugins'
7
+ import {
8
+ EscrowTermsSchema,
9
+ isSubtotal,
10
+ SubtotalSchema,
11
+ } from '@xyo-network/payment-payload-plugins'
12
+ import {
13
+ beforeAll, beforeEach, describe, expect,
14
+ it, vi,
15
+ } from 'vitest'
16
+
17
+ import { PaymentSubtotalDiviner } from '../Diviner.ts'
18
+
19
+ describe('PaymentSubtotalDiviner', () => {
20
+ let sut: PaymentSubtotalDiviner
21
+ const nbf = Date.now()
22
+ const exp = Number.MAX_SAFE_INTEGER
23
+ const termsBase: EscrowTerms = {
24
+ schema: EscrowTermsSchema, appraisals: [], exp, nbf,
25
+ }
26
+ const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [
27
+ [
28
+ [
29
+ {
30
+ schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf,
31
+ },
32
+ {
33
+ schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf,
34
+ },
35
+ ], 11],
36
+ [
37
+ [
38
+ {
39
+ schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf,
40
+ },
41
+ {
42
+ schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf,
43
+ },
44
+ ], 30],
45
+ [
46
+ [
47
+ {
48
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
49
+ },
50
+ {
51
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
52
+ },
53
+ {
54
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
55
+ },
56
+ ], 300],
57
+ ]
58
+ beforeEach(() => {
59
+ vi.clearAllMocks()
60
+ })
61
+ beforeAll(async () => {
62
+ sut = await PaymentSubtotalDiviner.create({ account: 'random' })
63
+ })
64
+ describe('with escrow terms containing valid appraisals', () => {
65
+ it.each(cases)('calculates the subtotal of all the appraisals', async (appraisals, total) => {
66
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
67
+ const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes }
68
+ const results = await sut.divine([terms, ...appraisals])
69
+ expect(results).toBeArrayOfSize(1)
70
+ const result = results.find(isSubtotal)
71
+ expect(result).toBeDefined()
72
+ expect(result).toMatchObject({ amount: total, schema: SubtotalSchema })
73
+ expect(result?.$sources).toEqual([await PayloadBuilder.dataHash(terms), ...appraisalHashes])
74
+ })
75
+ })
76
+ describe('with escrow terms containing invalid appraisals', () => {
77
+ describe('when containing negative values in appraisals', () => {
78
+ it('calculates the subtotal of all the appraisals', async () => {
79
+ const appraisals = [{
80
+ schema: HashLeaseEstimateSchema, price: -1, currency: 'USD', exp, nbf,
81
+ }]
82
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
83
+ const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes }
84
+ const results = await sut.divine([terms, ...appraisals])
85
+ expect(results).toBeArrayOfSize(0)
86
+ })
87
+ })
88
+ describe('when containing non-numeric values in appraisals', () => {
89
+ it('calculates the subtotal of all the appraisals', async () => {
90
+ const appraisals = [{
91
+ schema: HashLeaseEstimateSchema, price: 'three dollars', currency: 'USD', exp, nbf,
92
+ }]
93
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
94
+ const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes }
95
+ const results = await sut.divine([terms, ...appraisals])
96
+ expect(results).toBeArrayOfSize(0)
97
+ })
98
+ })
99
+ describe('when containing mixed currencies in appraisals', () => {
100
+ it('calculates the subtotal of all the appraisals', async () => {
101
+ const appraisals = [{
102
+ schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf,
103
+ }, {
104
+ schema: HashLeaseEstimateSchema, price: 1, currency: 'EUR', exp, nbf,
105
+ }]
106
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
107
+ const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes }
108
+ const results = await sut.divine([terms, ...appraisals])
109
+ expect(results).toBeArrayOfSize(0)
110
+ })
111
+ })
112
+ })
113
+ })
@@ -0,0 +1,173 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { MemoryArchivist } from '@xyo-network/archivist-memory'
4
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
5
+ import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory'
6
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
7
+ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease'
8
+ import { MemoryNode } from '@xyo-network/node-memory'
9
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
10
+ import type { Coupon, EscrowTerms } from '@xyo-network/payment-payload-plugins'
11
+ import {
12
+ EscrowTermsSchema,
13
+ FixedAmountCouponSchema, FixedPercentageCouponSchema, isTotal,
14
+ TotalSchema,
15
+ } from '@xyo-network/payment-payload-plugins'
16
+ import { HDWallet } from '@xyo-network/wallet'
17
+ import {
18
+ beforeAll, beforeEach, describe, expect,
19
+ it, vi,
20
+ } from 'vitest'
21
+
22
+ import { PaymentDiscountDiviner } from '../../Discount/index.ts'
23
+ import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts'
24
+ import { PaymentTotalDiviner } from '../Diviner.ts'
25
+
26
+ describe('PaymentTotalDiviner', () => {
27
+ let sut: PaymentTotalDiviner
28
+ const nbf = Date.now()
29
+ const exp = nbf + 1000 * 60 * 10
30
+ const termsBase: EscrowTerms = {
31
+ schema: EscrowTermsSchema, appraisals: [], exp, nbf,
32
+ }
33
+ const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [
34
+ [
35
+ [
36
+ {
37
+ schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf,
38
+ },
39
+ {
40
+ schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf,
41
+ },
42
+ ], 11],
43
+ [
44
+ [
45
+ {
46
+ schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf,
47
+ },
48
+ {
49
+ schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf,
50
+ },
51
+ ], 30],
52
+ [
53
+ [
54
+ {
55
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
56
+ },
57
+ {
58
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
59
+ },
60
+ {
61
+ schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf,
62
+ },
63
+ ], 300],
64
+ ]
65
+ const validCoupons: Coupon[] = [
66
+ {
67
+ amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD',
68
+ },
69
+ {
70
+ percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema,
71
+ },
72
+ ]
73
+ const unsignedCoupons: Coupon[] = [
74
+ {
75
+ amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD',
76
+ },
77
+ {
78
+ percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema,
79
+ },
80
+ ]
81
+ beforeEach(() => {
82
+ vi.clearAllMocks()
83
+ })
84
+ beforeAll(async () => {
85
+ const node = await MemoryNode.create({ account: 'random' })
86
+ expect(node).toBeDefined()
87
+ const paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' })
88
+ const discountsArchivist = await MemoryArchivist.create({ account: 'random' })
89
+ const signer = await HDWallet.random()
90
+ // Sign the valid coupons and insert them into the archivist
91
+ for (const coupon of validCoupons) {
92
+ const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build()
93
+ await discountsArchivist.insert([bw, ...payloads])
94
+ }
95
+ // Insert (but do not sign) the unsigned coupons into the archivist
96
+ await discountsArchivist.insert(unsignedCoupons)
97
+ const discountsBoundWitnessDiviner = await MemoryBoundWitnessDiviner.create({
98
+ account: 'random',
99
+ config: {
100
+ archivist: discountsArchivist.address,
101
+ schema: MemoryBoundWitnessDiviner.defaultConfigSchema,
102
+ },
103
+ })
104
+ const discountDiviner = await PaymentDiscountDiviner.create({
105
+ account: 'random',
106
+ config: {
107
+ archivist: discountsArchivist.address,
108
+ boundWitnessDiviner: discountsBoundWitnessDiviner.address,
109
+ couponAuthorities: [signer.address],
110
+ schema: PaymentDiscountDiviner.defaultConfigSchema,
111
+ },
112
+ })
113
+ sut = await PaymentTotalDiviner.create({
114
+ account: 'random',
115
+ config: {
116
+ paymentDiscountDiviner: discountDiviner.address,
117
+ paymentSubtotalDiviner: paymentSubtotalDiviner.address,
118
+ schema: PaymentTotalDiviner.defaultConfigSchema,
119
+ },
120
+ })
121
+
122
+ const modules = [paymentSubtotalDiviner, discountsArchivist, discountsBoundWitnessDiviner, discountDiviner, sut]
123
+ for (const mod of modules) {
124
+ await node.register(mod)
125
+ await node.attach(mod.address, true)
126
+ }
127
+ })
128
+ describe('with valid escrow', () => {
129
+ describe('with no discounts', () => {
130
+ it.each(cases)('calculates total', async (appraisals, total) => {
131
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
132
+ const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes }
133
+ const results = await sut.divine([terms, ...appraisals])
134
+ expect(results).toBeArrayOfSize(3)
135
+ const result = results.find(isTotal)
136
+ expect(result).toBeDefined()
137
+ expect(result).toMatchObject({ amount: total, schema: TotalSchema })
138
+ })
139
+ })
140
+ describe('with valid discounts', () => {
141
+ describe.each(cases)('calculates total', (appraisals, total) => {
142
+ it.each(validCoupons)('applying coupon discount to total', async (coupon) => {
143
+ const discounts = await PayloadBuilder.dataHashes([coupon])
144
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
145
+ const terms: EscrowTerms = {
146
+ ...termsBase, appraisals: appraisalHashes, discounts,
147
+ }
148
+ const results = await sut.divine([terms, ...appraisals, coupon])
149
+ expect(results).toBeArrayOfSize(3)
150
+ const result = results.find(isTotal)
151
+ expect(result).toBeDefined()
152
+ expect(result?.amount).toBeLessThan(total)
153
+ })
154
+ })
155
+ })
156
+ describe('with invalid discounts', () => {
157
+ describe.each(cases)('calculates total', (appraisals, total) => {
158
+ it.each(unsignedCoupons)('without applying coupon discount to total', async (coupon) => {
159
+ const discounts = await PayloadBuilder.dataHashes([coupon])
160
+ const appraisalHashes = await PayloadBuilder.dataHashes(appraisals)
161
+ const terms: EscrowTerms = {
162
+ ...termsBase, appraisals: appraisalHashes, discounts,
163
+ }
164
+ const results = await sut.divine([terms, ...appraisals, coupon])
165
+ expect(results).toBeArrayOfSize(3)
166
+ const result = results.find(isTotal)
167
+ expect(result).toBeDefined()
168
+ expect(result).toMatchObject({ amount: total, schema: TotalSchema })
169
+ })
170
+ })
171
+ })
172
+ })
173
+ })
package/typedoc.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "$schema": "https://typedoc.org/schema.json",
3
- "entryPoints": ["./src/index.ts"],
4
- "tsconfig": "./tsconfig.typedoc.json"
5
- }
package/xy.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { XyTsupConfig } from '@xylabs/ts-scripts-yarn3'
2
- const config: XyTsupConfig = {
3
- compile: {
4
- browser: {},
5
- node: {},
6
- neutral: { src: true },
7
- },
8
- }
9
-
10
- export default config