@xyo-network/payment-plugin 4.2.0 → 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/package.json +30 -26
- 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/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,35 +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": "^
|
|
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",
|
|
60
64
|
"typescript": "^5.8.3",
|
|
61
65
|
"vitest": "^3.2.4"
|
|
62
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