@gr4vy/sdk 2.1.2 → 2.1.3
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/README.md +8 -2
- package/jsr.json +1 -1
- package/lib/config.d.ts +2 -2
- package/lib/config.js +2 -2
- package/package.json +4 -2
- package/scripts/endpoint-coverage.mjs +136 -0
- package/src/lib/config.ts +2 -2
- package/tests/backoffice/account-updater.test.ts +24 -0
- package/tests/backoffice/audit-logs.test.ts +20 -0
- package/tests/backoffice/merchant-accounts.test.ts +77 -0
- package/tests/backoffice/payment-services.test.ts +95 -0
- package/tests/backoffice/payouts.test.ts +49 -0
- package/tests/backoffice/reports.test.ts +69 -0
- package/tests/backoffice/three-ds-scenarios.test.ts +34 -0
- package/tests/flows/buyer-lifecycle.test.ts +74 -0
- package/tests/flows/transaction-lifecycle.test.ts +151 -0
- package/tests/processing/buyers.test.ts +119 -0
- package/tests/processing/checkout-sessions.test.ts +99 -0
- package/tests/processing/digital-wallets.test.ts +154 -0
- package/tests/processing/gift-cards.test.ts +45 -0
- package/tests/processing/payment-links.test.ts +34 -0
- package/tests/processing/payment-methods.test.ts +121 -0
- package/tests/processing/transactions.test.ts +118 -0
- package/tests/utils/arbitraries.ts +87 -0
- package/tests/utils/fields.ts +65 -0
- package/tests/utils/fixtures.ts +84 -0
- package/tests/utils/poll.ts +40 -0
- package/tests/utils/setup.ts +66 -10
- package/tests/utils/transactions.ts +26 -0
- package/vitest.config.ts +31 -0
- package/tests/checkout-sessions.test.ts +0 -109
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { Gr4vy } from "../../src";
|
|
3
|
+
import { Transaction } from "../../src/models/components";
|
|
4
|
+
import {
|
|
5
|
+
APPROVING_CARD,
|
|
6
|
+
cartItem,
|
|
7
|
+
uniqueId,
|
|
8
|
+
unwrapTransaction,
|
|
9
|
+
} from "../utils/fixtures";
|
|
10
|
+
import { putCheckoutSessionCard } from "../utils/fields";
|
|
11
|
+
import { pollUntil } from "../utils/poll";
|
|
12
|
+
import { setupMerchant } from "../utils/setup";
|
|
13
|
+
|
|
14
|
+
let gr4vy: Gr4vy;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
({ client: gr4vy } = await setupMerchant());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Authorises a transaction the way a real integration does: open a checkout
|
|
22
|
+
* session, tokenise a card into it via the secure-fields endpoint, then create
|
|
23
|
+
* the transaction against that session.
|
|
24
|
+
*/
|
|
25
|
+
const authorize = async (
|
|
26
|
+
amount: number,
|
|
27
|
+
intent: "authorize" | "capture" = "authorize"
|
|
28
|
+
): Promise<Transaction> => {
|
|
29
|
+
const session = await gr4vy.checkoutSessions.create();
|
|
30
|
+
await putCheckoutSessionCard(session, APPROVING_CARD);
|
|
31
|
+
return gr4vy.transactions.create({
|
|
32
|
+
amount,
|
|
33
|
+
currency: "USD",
|
|
34
|
+
intent,
|
|
35
|
+
externalIdentifier: uniqueId("txn"),
|
|
36
|
+
cartItems: [cartItem({ unitAmount: amount })],
|
|
37
|
+
paymentMethod: { method: "checkout-session", id: session.id },
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("Transaction lifecycle", () => {
|
|
42
|
+
test("authorize → capture (full) → refund (partial then full)", async () => {
|
|
43
|
+
const transaction = await authorize(5000);
|
|
44
|
+
expect(transaction.status).toBe("authorization_succeeded");
|
|
45
|
+
expect(transaction.amount).toBe(5000);
|
|
46
|
+
|
|
47
|
+
// Capture the full amount.
|
|
48
|
+
const captured = unwrapTransaction(
|
|
49
|
+
await gr4vy.transactions.capture({
|
|
50
|
+
transactionId: transaction.id,
|
|
51
|
+
transactionCaptureCreate: { amount: 5000 },
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
expect(captured.id).toBe(transaction.id);
|
|
55
|
+
|
|
56
|
+
const settled = await pollUntil(
|
|
57
|
+
() => gr4vy.transactions.get(transaction.id),
|
|
58
|
+
(t) => t.status === "capture_succeeded",
|
|
59
|
+
{ description: "capture to succeed" }
|
|
60
|
+
);
|
|
61
|
+
expect(settled.capturedAmount).toBe(5000);
|
|
62
|
+
|
|
63
|
+
// Partial refund, then refund the remainder.
|
|
64
|
+
const partialRefund = await gr4vy.transactions.refunds.create(
|
|
65
|
+
{ amount: 2000, reason: "partial" },
|
|
66
|
+
transaction.id
|
|
67
|
+
);
|
|
68
|
+
expect(partialRefund.amount).toBe(2000);
|
|
69
|
+
expect(["processing", "succeeded"]).toContain(partialRefund.status);
|
|
70
|
+
|
|
71
|
+
const fullRefund = await gr4vy.transactions.refunds.all.create(transaction.id);
|
|
72
|
+
expect(fullRefund.items.length).toBeGreaterThanOrEqual(1);
|
|
73
|
+
|
|
74
|
+
// Read the refund back through both refund accessors.
|
|
75
|
+
const refunds = await gr4vy.transactions.refunds.list(transaction.id);
|
|
76
|
+
expect(refunds.items.length).toBeGreaterThanOrEqual(1);
|
|
77
|
+
const firstRefundId = refunds.items[0]!.id;
|
|
78
|
+
const fetched = await gr4vy.transactions.refunds.get(
|
|
79
|
+
transaction.id,
|
|
80
|
+
firstRefundId
|
|
81
|
+
);
|
|
82
|
+
expect(fetched.id).toBe(firstRefundId);
|
|
83
|
+
const topLevel = await gr4vy.refunds.get(firstRefundId);
|
|
84
|
+
expect(topLevel.id).toBe(firstRefundId);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("authorize → void releases the authorization", async () => {
|
|
88
|
+
const transaction = await authorize(3300);
|
|
89
|
+
expect(transaction.status).toBe("authorization_succeeded");
|
|
90
|
+
|
|
91
|
+
const voided = unwrapTransaction(
|
|
92
|
+
await gr4vy.transactions.void(transaction.id)
|
|
93
|
+
);
|
|
94
|
+
expect(voided.id).toBe(transaction.id);
|
|
95
|
+
|
|
96
|
+
const after = await pollUntil(
|
|
97
|
+
() => gr4vy.transactions.get(transaction.id),
|
|
98
|
+
(t) =>
|
|
99
|
+
t.status === "authorization_voided" ||
|
|
100
|
+
t.status === "authorization_void_pending",
|
|
101
|
+
{ description: "void to be reflected" }
|
|
102
|
+
);
|
|
103
|
+
expect(["authorization_voided", "authorization_void_pending"]).toContain(
|
|
104
|
+
after.status
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// `cancel` is not supported by the mock-card connector, so the call is
|
|
109
|
+
// exercised at the request level: the SDK forms the request and the API
|
|
110
|
+
// rejects it with a clear "not supported" error.
|
|
111
|
+
test("authorize → cancel is exercised at the request level", async () => {
|
|
112
|
+
const transaction = await authorize(1500);
|
|
113
|
+
await expect(gr4vy.transactions.cancel(transaction.id)).rejects.toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("intent=capture authorizes and captures in one step", async () => {
|
|
117
|
+
const transaction = await authorize(2500, "capture");
|
|
118
|
+
expect(["capture_succeeded", "capture_pending"]).toContain(
|
|
119
|
+
transaction.status
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("exposes actions, events and settlements for a transaction", async () => {
|
|
124
|
+
const transaction = await authorize(4200, "capture");
|
|
125
|
+
|
|
126
|
+
const actions = await gr4vy.transactions.actions.list(transaction.id);
|
|
127
|
+
expect(Array.isArray(actions.items)).toBe(true);
|
|
128
|
+
|
|
129
|
+
const events = await gr4vy.transactions.events.list(transaction.id);
|
|
130
|
+
expect(events).toBeDefined();
|
|
131
|
+
|
|
132
|
+
const settlements = await gr4vy.transactions.settlements.list(transaction.id);
|
|
133
|
+
expect(settlements).toBeDefined();
|
|
134
|
+
|
|
135
|
+
// No settlement exists yet for a freshly captured transaction in the mock
|
|
136
|
+
// env, so fetching one by id is exercised at the request level.
|
|
137
|
+
await expect(
|
|
138
|
+
gr4vy.transactions.settlements.get(
|
|
139
|
+
transaction.id,
|
|
140
|
+
"00000000-0000-0000-0000-000000000000"
|
|
141
|
+
)
|
|
142
|
+
).rejects.toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// `sync` is not supported by the mock-card connector; exercise the call shape
|
|
146
|
+
// and assert the API rejects it rather than skipping the endpoint.
|
|
147
|
+
test("sync is exercised at the request level", async () => {
|
|
148
|
+
const transaction = await authorize(1700);
|
|
149
|
+
await expect(gr4vy.transactions.sync(transaction.id)).rejects.toThrow();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
3
|
+
import { Gr4vy } from "../../src";
|
|
4
|
+
import { address, billingDetails, buyer, uniqueId } from "../utils/fixtures";
|
|
5
|
+
import { fcParams, name } from "../utils/arbitraries";
|
|
6
|
+
import { setupMerchant } from "../utils/setup";
|
|
7
|
+
|
|
8
|
+
let gr4vy: Gr4vy;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
({ client: gr4vy } = await setupMerchant());
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("Buyers", () => {
|
|
15
|
+
test("create → get → list → delete", async () => {
|
|
16
|
+
const created = await gr4vy.buyers.create(buyer());
|
|
17
|
+
expect(created.id).toBeDefined();
|
|
18
|
+
|
|
19
|
+
const fetched = await gr4vy.buyers.get(created.id!);
|
|
20
|
+
expect(fetched.id).toBe(created.id);
|
|
21
|
+
|
|
22
|
+
const page = await gr4vy.buyers.list({ search: created.id });
|
|
23
|
+
expect(page).toBeDefined();
|
|
24
|
+
|
|
25
|
+
await gr4vy.buyers.delete(created.id!);
|
|
26
|
+
await expect(() => gr4vy.buyers.get(created.id!)).rejects.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("can be looked up by external identifier", async () => {
|
|
30
|
+
const externalIdentifier = uniqueId("buyer-ext");
|
|
31
|
+
const created = await gr4vy.buyers.create(buyer({ externalIdentifier }));
|
|
32
|
+
expect(created.externalIdentifier).toBe(externalIdentifier);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Property: a partial update changes only the fields supplied and leaves every
|
|
36
|
+
// other field untouched. The subset of fields to mutate is itself generated,
|
|
37
|
+
// so we cover many partial-update combinations cheaply against the live API.
|
|
38
|
+
test("partial update only touches the supplied fields", async () => {
|
|
39
|
+
await fc.assert(
|
|
40
|
+
fc.asyncProperty(
|
|
41
|
+
fc.record(
|
|
42
|
+
{
|
|
43
|
+
displayName: name(),
|
|
44
|
+
accountNumber: fc.string({ minLength: 1, maxLength: 12 }),
|
|
45
|
+
},
|
|
46
|
+
{ requiredKeys: [] }
|
|
47
|
+
),
|
|
48
|
+
async (patch) => {
|
|
49
|
+
fc.pre(Object.keys(patch).length > 0);
|
|
50
|
+
|
|
51
|
+
const original = await gr4vy.buyers.create(
|
|
52
|
+
buyer({
|
|
53
|
+
displayName: "Original Name",
|
|
54
|
+
accountNumber: "ACC-ORIGINAL",
|
|
55
|
+
billingDetails: billingDetails({ firstName: "Keep", lastName: "Me" }),
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const updated = await gr4vy.buyers.update(patch, original.id!);
|
|
60
|
+
|
|
61
|
+
// Changed fields took effect.
|
|
62
|
+
if (patch.displayName !== undefined) {
|
|
63
|
+
expect(updated.displayName).toBe(patch.displayName);
|
|
64
|
+
}
|
|
65
|
+
if (patch.accountNumber !== undefined) {
|
|
66
|
+
expect(updated.accountNumber).toBe(patch.accountNumber);
|
|
67
|
+
}
|
|
68
|
+
// Untouched fields retained their original values.
|
|
69
|
+
if (patch.displayName === undefined) {
|
|
70
|
+
expect(updated.displayName).toBe("Original Name");
|
|
71
|
+
}
|
|
72
|
+
if (patch.accountNumber === undefined) {
|
|
73
|
+
expect(updated.accountNumber).toBe("ACC-ORIGINAL");
|
|
74
|
+
}
|
|
75
|
+
// Billing details were never in the patch, so they must be preserved.
|
|
76
|
+
expect(updated.billingDetails?.firstName).toBe("Keep");
|
|
77
|
+
expect(updated.billingDetails?.lastName).toBe("Me");
|
|
78
|
+
|
|
79
|
+
await gr4vy.buyers.delete(original.id!);
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
fcParams(4)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("shipping details", () => {
|
|
87
|
+
test("create → list → get → update (partial) → delete", async () => {
|
|
88
|
+
const owner = await gr4vy.buyers.create(buyer());
|
|
89
|
+
|
|
90
|
+
const created = await gr4vy.buyers.shippingDetails.create(
|
|
91
|
+
{
|
|
92
|
+
firstName: "Ship",
|
|
93
|
+
lastName: "Here",
|
|
94
|
+
emailAddress: "ship@example.com",
|
|
95
|
+
address: address({ city: "London" }),
|
|
96
|
+
},
|
|
97
|
+
owner.id!
|
|
98
|
+
);
|
|
99
|
+
expect(created.id).toBeDefined();
|
|
100
|
+
|
|
101
|
+
const list = await gr4vy.buyers.shippingDetails.list(owner.id!);
|
|
102
|
+
expect(list.items.some((s) => s.id === created.id)).toBe(true);
|
|
103
|
+
|
|
104
|
+
const fetched = await gr4vy.buyers.shippingDetails.get(owner.id!, created.id!);
|
|
105
|
+
expect(fetched.id).toBe(created.id);
|
|
106
|
+
|
|
107
|
+
// Partial update: change only the first name.
|
|
108
|
+
const updated = await gr4vy.buyers.shippingDetails.update(
|
|
109
|
+
{ firstName: "Renamed" },
|
|
110
|
+
owner.id!,
|
|
111
|
+
created.id!
|
|
112
|
+
);
|
|
113
|
+
expect(updated.firstName).toBe("Renamed");
|
|
114
|
+
expect(updated.lastName).toBe("Here");
|
|
115
|
+
|
|
116
|
+
await gr4vy.buyers.shippingDetails.delete(owner.id!, created.id!);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { Gr4vy } from "../../src";
|
|
3
|
+
import { CardPaymentMethodCreate } from "../../src/models/components";
|
|
4
|
+
import {
|
|
5
|
+
APPROVING_CARD,
|
|
6
|
+
cardPaymentMethod,
|
|
7
|
+
cartItem,
|
|
8
|
+
} from "../utils/fixtures";
|
|
9
|
+
import {
|
|
10
|
+
putCheckoutSessionCard,
|
|
11
|
+
putCheckoutSessionStoredMethod,
|
|
12
|
+
} from "../utils/fields";
|
|
13
|
+
import { setupMerchant } from "../utils/setup";
|
|
14
|
+
|
|
15
|
+
let gr4vy: Gr4vy;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
({ client: gr4vy } = await setupMerchant());
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Checkout Sessions", () => {
|
|
22
|
+
test("should process a payment with a checkout session", async () => {
|
|
23
|
+
const checkoutSession = await gr4vy.checkoutSessions.create();
|
|
24
|
+
expect(checkoutSession.id).toBeDefined();
|
|
25
|
+
|
|
26
|
+
await putCheckoutSessionCard(checkoutSession, APPROVING_CARD);
|
|
27
|
+
|
|
28
|
+
const transaction = await gr4vy.transactions.create({
|
|
29
|
+
amount: 1299,
|
|
30
|
+
currency: "USD",
|
|
31
|
+
paymentMethod: { method: "checkout-session", id: checkoutSession.id },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(transaction.id).toBeDefined();
|
|
35
|
+
expect(transaction.status).toBe("authorization_succeeded");
|
|
36
|
+
expect(transaction.amount).toBe(1299);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("should handle the error raised on missing card data", async () => {
|
|
40
|
+
const checkoutSession = await gr4vy.checkoutSessions.create();
|
|
41
|
+
expect(checkoutSession.id).toBeDefined();
|
|
42
|
+
|
|
43
|
+
const request = {
|
|
44
|
+
amount: 1299,
|
|
45
|
+
currency: "USD",
|
|
46
|
+
paymentMethod: {
|
|
47
|
+
method: "checkout-session" as const,
|
|
48
|
+
id: checkoutSession.id,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
await expect(() => gr4vy.transactions.create(request)).rejects.toThrowError(
|
|
52
|
+
"Request failed validation"
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("should handle a stored payment method", async () => {
|
|
57
|
+
const request: CardPaymentMethodCreate = cardPaymentMethod();
|
|
58
|
+
const paymentMethod = await gr4vy.paymentMethods.create(request);
|
|
59
|
+
expect(paymentMethod.id).toBeDefined();
|
|
60
|
+
|
|
61
|
+
const checkoutSession = await gr4vy.checkoutSessions.create();
|
|
62
|
+
await putCheckoutSessionStoredMethod(
|
|
63
|
+
checkoutSession,
|
|
64
|
+
paymentMethod.id,
|
|
65
|
+
"123"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const transaction = await gr4vy.transactions.create({
|
|
69
|
+
amount: 1299,
|
|
70
|
+
currency: "USD",
|
|
71
|
+
paymentMethod: { method: "checkout-session", id: checkoutSession.id },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(transaction.id).toBeDefined();
|
|
75
|
+
expect(transaction.status).toBe("authorization_succeeded");
|
|
76
|
+
expect(transaction.amount).toBe(1299);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("create → get → update (partial) → delete", async () => {
|
|
80
|
+
const created = await gr4vy.checkoutSessions.create({
|
|
81
|
+
cartItems: [cartItem()],
|
|
82
|
+
metadata: { source: "test" },
|
|
83
|
+
});
|
|
84
|
+
expect(created.id).toBeDefined();
|
|
85
|
+
|
|
86
|
+
const fetched = await gr4vy.checkoutSessions.get(created.id);
|
|
87
|
+
expect(fetched.id).toBe(created.id);
|
|
88
|
+
|
|
89
|
+
// Partial update: change only the metadata, leave the cart items.
|
|
90
|
+
const updated = await gr4vy.checkoutSessions.update(
|
|
91
|
+
{ metadata: { source: "updated" } },
|
|
92
|
+
created.id
|
|
93
|
+
);
|
|
94
|
+
expect(updated.metadata?.["source"]).toBe("updated");
|
|
95
|
+
|
|
96
|
+
await gr4vy.checkoutSessions.delete(created.id);
|
|
97
|
+
await expect(() => gr4vy.checkoutSessions.get(created.id)).rejects.toThrow();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { Gr4vy } from "../../src";
|
|
3
|
+
import { DigitalWallet } from "../../src/models/components";
|
|
4
|
+
import { uniqueId } from "../utils/fixtures";
|
|
5
|
+
import { setupMerchant } from "../utils/setup";
|
|
6
|
+
|
|
7
|
+
let gr4vy: Gr4vy;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
({ client: gr4vy } = await setupMerchant());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const registerGoogleWallet = () =>
|
|
14
|
+
gr4vy.digitalWallets.create({
|
|
15
|
+
provider: "google",
|
|
16
|
+
merchantName: uniqueId("gr4vy-e2e"),
|
|
17
|
+
merchantDisplayName: "Gr4vy E2E",
|
|
18
|
+
acceptTermsAndConditions: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// A merchant allows only one wallet per provider, so register a wallet, run the
|
|
22
|
+
// callback against it, then always remove it — keeping the tests isolated even
|
|
23
|
+
// though they share a merchant account.
|
|
24
|
+
const withWallet = async (
|
|
25
|
+
fn: (wallet: DigitalWallet) => Promise<void>
|
|
26
|
+
): Promise<void> => {
|
|
27
|
+
const wallet = await registerGoogleWallet();
|
|
28
|
+
try {
|
|
29
|
+
await fn(wallet);
|
|
30
|
+
} finally {
|
|
31
|
+
await gr4vy.digitalWallets.delete(wallet.id);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("Digital Wallets", () => {
|
|
36
|
+
test("register → get → list → update (partial) → delete", async () => {
|
|
37
|
+
const created = await registerGoogleWallet();
|
|
38
|
+
expect(created.id).toBeDefined();
|
|
39
|
+
|
|
40
|
+
const fetched = await gr4vy.digitalWallets.get(created.id);
|
|
41
|
+
expect(fetched.id).toBe(created.id);
|
|
42
|
+
|
|
43
|
+
const list = await gr4vy.digitalWallets.list();
|
|
44
|
+
expect(list.items.some((w) => w.id === created.id)).toBe(true);
|
|
45
|
+
|
|
46
|
+
// Partial update: change only the display name.
|
|
47
|
+
const updated = await gr4vy.digitalWallets.update(
|
|
48
|
+
{ merchantDisplayName: "Renamed Display" },
|
|
49
|
+
created.id
|
|
50
|
+
);
|
|
51
|
+
expect(updated.merchantDisplayName).toBe("Renamed Display");
|
|
52
|
+
|
|
53
|
+
await gr4vy.digitalWallets.delete(created.id);
|
|
54
|
+
await expect(() => gr4vy.digitalWallets.get(created.id)).rejects.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// A Google Pay session can be minted server-side from an origin domain; assert
|
|
58
|
+
// the SDK returns the gateway merchant id + session token.
|
|
59
|
+
test("creates a google pay session", async () => {
|
|
60
|
+
const session = await gr4vy.digitalWallets.sessions.googlePay({
|
|
61
|
+
originDomain: "not-a-registered-domain.example.com",
|
|
62
|
+
});
|
|
63
|
+
expect(session).toHaveProperty("token");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// The remaining wallet sessions (Apple Pay, Click to Pay, Paze + Paze mobile)
|
|
67
|
+
// need a verified wallet / real device payloads that can't be minted on the
|
|
68
|
+
// mock merchant. Each is exercised at the request level so the SDK request
|
|
69
|
+
// shape is covered and the API rejects it for a real reason.
|
|
70
|
+
describe("sessions exercised at the request level", () => {
|
|
71
|
+
test("apple pay session", async () => {
|
|
72
|
+
await expect(
|
|
73
|
+
gr4vy.digitalWallets.sessions.applePay({
|
|
74
|
+
validationUrl: "https://apple-pay-gateway.apple.com/paymentservices",
|
|
75
|
+
domainName: "not-a-registered-domain.example.com",
|
|
76
|
+
})
|
|
77
|
+
).rejects.toThrow();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("click to pay session", async () => {
|
|
81
|
+
await expect(
|
|
82
|
+
gr4vy.digitalWallets.sessions.clickToPay({
|
|
83
|
+
checkoutSessionId: "00000000-0000-0000-0000-000000000000",
|
|
84
|
+
})
|
|
85
|
+
).rejects.toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("paze session", async () => {
|
|
89
|
+
await expect(
|
|
90
|
+
gr4vy.digitalWallets.sessions.paze({
|
|
91
|
+
source: "web",
|
|
92
|
+
domainName: "not-a-registered-domain.example.com",
|
|
93
|
+
})
|
|
94
|
+
).rejects.toThrow();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("paze mobile session create / review / complete", async () => {
|
|
98
|
+
await expect(
|
|
99
|
+
gr4vy.digitalWallets.sessions.pazeMobileSessionCreate({
|
|
100
|
+
client: { id: "client-id" },
|
|
101
|
+
sessionId: "session-id",
|
|
102
|
+
accessToken: "access-token",
|
|
103
|
+
callbackURLScheme: "gr4vy",
|
|
104
|
+
intent: "EXPRESS_CHECKOUT",
|
|
105
|
+
})
|
|
106
|
+
).rejects.toThrow();
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
gr4vy.digitalWallets.sessions.pazeMobileSessionReview({
|
|
110
|
+
sessionId: "session-id",
|
|
111
|
+
code: "code",
|
|
112
|
+
accessToken: "access-token",
|
|
113
|
+
})
|
|
114
|
+
).rejects.toThrow();
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
gr4vy.digitalWallets.sessions.pazeMobileSessionComplete({
|
|
118
|
+
sessionId: "session-id",
|
|
119
|
+
code: "code",
|
|
120
|
+
accessToken: "access-token",
|
|
121
|
+
transactionType: "PURCHASE",
|
|
122
|
+
})
|
|
123
|
+
).rejects.toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Apple Pay domain registration needs an Apple-provider wallet onboarded with
|
|
128
|
+
// Apple; on the mock merchant both register and remove are rejected, which
|
|
129
|
+
// still exercises the domain create/delete request shapes.
|
|
130
|
+
describe("domains exercised at the request level", () => {
|
|
131
|
+
test("domain registration is rejected", async () => {
|
|
132
|
+
await withWallet(async (wallet) => {
|
|
133
|
+
await expect(
|
|
134
|
+
gr4vy.digitalWallets.domains.create(
|
|
135
|
+
{ domainName: "example.com" },
|
|
136
|
+
wallet.id
|
|
137
|
+
)
|
|
138
|
+
).rejects.toThrow();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("domain removal is idempotent", async () => {
|
|
143
|
+
await withWallet(async (wallet) => {
|
|
144
|
+
// Removing a domain that was never registered resolves without error.
|
|
145
|
+
await expect(
|
|
146
|
+
gr4vy.digitalWallets.domains.delete(
|
|
147
|
+
{ domainName: "example.com" },
|
|
148
|
+
wallet.id
|
|
149
|
+
)
|
|
150
|
+
).resolves.toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { Gr4vy } from "../../src";
|
|
3
|
+
import { setupMerchant } from "../utils/setup";
|
|
4
|
+
|
|
5
|
+
let gr4vy: Gr4vy;
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
({ client: gr4vy } = await setupMerchant());
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("Gift Cards", () => {
|
|
12
|
+
test("listing gift cards returns a page", async () => {
|
|
13
|
+
const page = await gr4vy.giftCards.list({});
|
|
14
|
+
expect(page).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Creating a gift card and reading its balance requires a gift-card service
|
|
18
|
+
// (e.g. `mock-gift-card`) to be configured on the merchant. The mock merchant
|
|
19
|
+
// is provisioned only with `mock-card`, so these calls are exercised at the
|
|
20
|
+
// request level and the API is expected to reject them for a real reason.
|
|
21
|
+
test("create is exercised at the request level", async () => {
|
|
22
|
+
await expect(
|
|
23
|
+
gr4vy.giftCards.create({
|
|
24
|
+
number: "4111111111111111",
|
|
25
|
+
pin: "1234",
|
|
26
|
+
})
|
|
27
|
+
).rejects.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("balance lookup is exercised at the request level", async () => {
|
|
31
|
+
await expect(
|
|
32
|
+
gr4vy.giftCards.balances.list({
|
|
33
|
+
items: [{ number: "4111111111111111", pin: "1234" }],
|
|
34
|
+
})
|
|
35
|
+
).rejects.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// No gift card exists to fetch/delete on the mock merchant, so get/delete are
|
|
39
|
+
// exercised against a non-existent id and expected to be rejected.
|
|
40
|
+
test("get and delete are exercised at the request level", async () => {
|
|
41
|
+
const bogus = "00000000-0000-0000-0000-000000000000";
|
|
42
|
+
await expect(gr4vy.giftCards.get(bogus)).rejects.toThrow();
|
|
43
|
+
await expect(gr4vy.giftCards.delete(bogus)).rejects.toThrow();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { Gr4vy } from "../../src";
|
|
3
|
+
import { cartItem, uniqueId } from "../utils/fixtures";
|
|
4
|
+
import { setupMerchant } from "../utils/setup";
|
|
5
|
+
|
|
6
|
+
let gr4vy: Gr4vy;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
({ client: gr4vy } = await setupMerchant());
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("Payment Links", () => {
|
|
13
|
+
test("create → get → list → expire", async () => {
|
|
14
|
+
const created = await gr4vy.paymentLinks.create({
|
|
15
|
+
amount: 1299,
|
|
16
|
+
currency: "USD",
|
|
17
|
+
country: "US",
|
|
18
|
+
externalIdentifier: uniqueId("plink"),
|
|
19
|
+
cartItems: [cartItem()],
|
|
20
|
+
merchantName: "Gr4vy E2E",
|
|
21
|
+
});
|
|
22
|
+
expect(created.id).toBeDefined();
|
|
23
|
+
|
|
24
|
+
const fetched = await gr4vy.paymentLinks.get(created.id);
|
|
25
|
+
expect(fetched.id).toBe(created.id);
|
|
26
|
+
|
|
27
|
+
const page = await gr4vy.paymentLinks.list();
|
|
28
|
+
expect(page).toBeDefined();
|
|
29
|
+
|
|
30
|
+
await gr4vy.paymentLinks.expire(created.id);
|
|
31
|
+
const afterExpiry = await gr4vy.paymentLinks.get(created.id);
|
|
32
|
+
expect(afterExpiry.status).toBe("expired");
|
|
33
|
+
});
|
|
34
|
+
});
|