@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.
- package/dist/neutral/index.d.ts +5 -88
- package/package.json +30 -27
- package/src/Discount/lib/spec/applyCoupons.spec.ts +104 -0
- package/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts +243 -0
- package/src/Discount/spec/Diviner.spec.ts +128 -0
- package/src/Invoice/spec/getInvoiceForEscrow.spec.ts +94 -0
- package/src/Subtotal/spec/Diviner.spec.ts +113 -0
- package/src/Total/spec/Diviner.spec.ts +173 -0
- package/typedoc.json +0 -5
- package/xy.config.ts +0 -10
package/dist/neutral/index.d.ts
CHANGED
|
@@ -1,88 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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": "
|
|
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": "^
|
|
33
|
-
"@xylabs/assert": "^
|
|
34
|
-
"@xylabs/exists": "^
|
|
35
|
-
"@xylabs/hex": "^
|
|
36
|
-
"@xyo-network/archivist-model": "^
|
|
37
|
-
"@xyo-network/diviner-abstract": "^
|
|
38
|
-
"@xyo-network/diviner-boundwitness-model": "^
|
|
39
|
-
"@xyo-network/diviner-hash-lease": "^
|
|
40
|
-
"@xyo-network/diviner-model": "^
|
|
41
|
-
"@xyo-network/module-model": "^
|
|
42
|
-
"@xyo-network/payload-builder": "^
|
|
43
|
-
"@xyo-network/payload-model": "^
|
|
44
|
-
"@xyo-network/payment-payload-plugins": "^
|
|
45
|
-
"@xyo-network/schema-payload-plugin": "^
|
|
46
|
-
"@xyo-network/xns-record-payload-plugins": "^
|
|
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.
|
|
51
|
-
"@xylabs/tsconfig": "^7.0.
|
|
52
|
-
"@xylabs/vitest-extended": "^
|
|
53
|
-
"@xyo-network/archivist-memory": "^
|
|
54
|
-
"@xyo-network/boundwitness-builder": "^
|
|
55
|
-
"@xyo-network/diviner-boundwitness-memory": "^
|
|
56
|
-
"@xyo-network/id-payload-plugin": "^
|
|
57
|
-
"@xyo-network/node-memory": "^
|
|
58
|
-
"@xyo-network/wallet": "^
|
|
59
|
-
"@xyo-network/wallet-model": "^
|
|
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