@gr4vy/sdk 2.1.1 → 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.
Files changed (46) hide show
  1. package/README.md +8 -2
  2. package/jsr.json +1 -1
  3. package/lib/config.d.ts +2 -2
  4. package/lib/config.js +2 -2
  5. package/models/components/method.d.ts +2 -0
  6. package/models/components/method.d.ts.map +1 -1
  7. package/models/components/method.js +2 -0
  8. package/models/components/method.js.map +1 -1
  9. package/models/components/redirectpaymentmethodcreate.d.ts +2 -0
  10. package/models/components/redirectpaymentmethodcreate.d.ts.map +1 -1
  11. package/models/components/redirectpaymentmethodcreate.js +2 -0
  12. package/models/components/redirectpaymentmethodcreate.js.map +1 -1
  13. package/models/components/transactionconnectionoptions.d.ts +10 -0
  14. package/models/components/transactionconnectionoptions.d.ts.map +1 -1
  15. package/models/components/transactionconnectionoptions.js +4 -0
  16. package/models/components/transactionconnectionoptions.js.map +1 -1
  17. package/package.json +4 -2
  18. package/scripts/endpoint-coverage.mjs +136 -0
  19. package/src/lib/config.ts +2 -2
  20. package/src/models/components/method.ts +2 -0
  21. package/src/models/components/redirectpaymentmethodcreate.ts +2 -0
  22. package/src/models/components/transactionconnectionoptions.ts +14 -0
  23. package/tests/backoffice/account-updater.test.ts +24 -0
  24. package/tests/backoffice/audit-logs.test.ts +20 -0
  25. package/tests/backoffice/merchant-accounts.test.ts +77 -0
  26. package/tests/backoffice/payment-services.test.ts +95 -0
  27. package/tests/backoffice/payouts.test.ts +49 -0
  28. package/tests/backoffice/reports.test.ts +69 -0
  29. package/tests/backoffice/three-ds-scenarios.test.ts +34 -0
  30. package/tests/flows/buyer-lifecycle.test.ts +74 -0
  31. package/tests/flows/transaction-lifecycle.test.ts +151 -0
  32. package/tests/processing/buyers.test.ts +119 -0
  33. package/tests/processing/checkout-sessions.test.ts +99 -0
  34. package/tests/processing/digital-wallets.test.ts +154 -0
  35. package/tests/processing/gift-cards.test.ts +45 -0
  36. package/tests/processing/payment-links.test.ts +34 -0
  37. package/tests/processing/payment-methods.test.ts +121 -0
  38. package/tests/processing/transactions.test.ts +118 -0
  39. package/tests/utils/arbitraries.ts +87 -0
  40. package/tests/utils/fields.ts +65 -0
  41. package/tests/utils/fixtures.ts +84 -0
  42. package/tests/utils/poll.ts +40 -0
  43. package/tests/utils/setup.ts +66 -10
  44. package/tests/utils/transactions.ts +26 -0
  45. package/vitest.config.ts +31 -0
  46. package/tests/checkout-sessions.test.ts +0 -109
@@ -0,0 +1,77 @@
1
+ import crypto from "crypto";
2
+ import { beforeAll, describe, expect, test } from "vitest";
3
+ import { Gr4vy } from "../../src";
4
+ import { createGr4vyClient, setupMerchant } from "../utils/setup";
5
+
6
+ let admin: Gr4vy;
7
+ let privateKey: string;
8
+
9
+ beforeAll(async () => {
10
+ // merchant-accounts are an admin-level resource, so use an un-scoped client.
11
+ ({ privateKey } = await setupMerchant());
12
+ admin = createGr4vyClient(privateKey);
13
+ });
14
+
15
+ const newMerchantId = () => crypto.randomBytes(8).toString("hex");
16
+
17
+ describe("Merchant Accounts", () => {
18
+ test("create → get → list → update (partial)", async () => {
19
+ const id = newMerchantId();
20
+ const created = await admin.merchantAccounts.create({
21
+ id,
22
+ displayName: "Original Display",
23
+ });
24
+ expect(created.id).toBe(id);
25
+
26
+ const fetched = await admin.merchantAccounts.get(id);
27
+ expect(fetched.id).toBe(id);
28
+
29
+ const page = await admin.merchantAccounts.list();
30
+ expect(page).toBeDefined();
31
+
32
+ // Partial update: change only the display name, leave everything else.
33
+ const updated = await admin.merchantAccounts.update(
34
+ { displayName: "Renamed Display" },
35
+ id
36
+ );
37
+ expect(updated.displayName).toBe("Renamed Display");
38
+ expect(updated.id).toBe(id);
39
+ });
40
+
41
+ describe("3DS configuration", () => {
42
+ // 3DS configuration is keyed per scheme/currency on the merchant account.
43
+ test("create → list → update (partial) → delete", async () => {
44
+ const id = newMerchantId();
45
+ await admin.merchantAccounts.create({ id, displayName: id });
46
+
47
+ const created =
48
+ await admin.merchantAccounts.threeDsConfiguration.create(
49
+ {
50
+ merchantAcquirerBin: "516327",
51
+ merchantAcquirerId: "123456789012345",
52
+ merchantName: "Gr4vy E2E",
53
+ merchantCountryCode: "840",
54
+ merchantCategoryCode: "1234",
55
+ merchantUrl: "https://example.com",
56
+ scheme: "visa",
57
+ metadata: { env: "e2e" },
58
+ },
59
+ id
60
+ );
61
+ expect(created.id).toBeDefined();
62
+
63
+ const list = await admin.merchantAccounts.threeDsConfiguration.list(id);
64
+ expect(list).toBeDefined();
65
+
66
+ const updated =
67
+ await admin.merchantAccounts.threeDsConfiguration.update(
68
+ { merchantName: "Renamed Merchant" },
69
+ id,
70
+ created.id!
71
+ );
72
+ expect(updated.merchantName).toBe("Renamed Merchant");
73
+
74
+ await admin.merchantAccounts.threeDsConfiguration.delete(id, created.id!);
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,95 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { PaymentServiceCreate } from "../../src/models/components";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ const mockCardService = (
13
+ overrides: Partial<PaymentServiceCreate> = {}
14
+ ): PaymentServiceCreate => ({
15
+ displayName: "Mock card service",
16
+ paymentServiceDefinitionId: "mock-card",
17
+ acceptedCurrencies: ["USD"],
18
+ acceptedCountries: ["US"],
19
+ fields: [{ key: "merchant_id", value: "test" }],
20
+ ...overrides,
21
+ });
22
+
23
+ describe("Payment Services", () => {
24
+ test("create → get → list → update (partial) → delete", async () => {
25
+ const created = await gr4vy.paymentServices.create(mockCardService());
26
+ expect(created.id).toBeDefined();
27
+
28
+ const fetched = await gr4vy.paymentServices.get(created.id!);
29
+ expect(fetched.id).toBe(created.id);
30
+
31
+ const page = await gr4vy.paymentServices.list({});
32
+ expect(page).toBeDefined();
33
+
34
+ // Partial update: change only the display name.
35
+ const updated = await gr4vy.paymentServices.update(
36
+ { displayName: "Renamed service" },
37
+ created.id!
38
+ );
39
+ expect(updated.displayName).toBe("Renamed service");
40
+
41
+ await gr4vy.paymentServices.delete(created.id!);
42
+ await expect(() => gr4vy.paymentServices.get(created.id!)).rejects.toThrow();
43
+ });
44
+
45
+ // Credential verification and session creation are not meaningfully supported
46
+ // by the mock-card connector (verify returns a non-JSON body; session is
47
+ // explicitly unsupported), so both are exercised at the request level.
48
+ test("verify is exercised at the request level", async () => {
49
+ await expect(
50
+ gr4vy.paymentServices.verify({
51
+ paymentServiceDefinitionId: "mock-card",
52
+ fields: [{ key: "merchant_id", value: "test" }],
53
+ })
54
+ ).rejects.toThrow();
55
+ });
56
+
57
+ test("create a session is exercised at the request level", async () => {
58
+ const created = await gr4vy.paymentServices.create(mockCardService());
59
+ await expect(
60
+ gr4vy.paymentServices.session({}, created.id!)
61
+ ).rejects.toThrow();
62
+ });
63
+
64
+ describe("definitions, options and card schemes", () => {
65
+ test("list and get payment service definitions", async () => {
66
+ const page = await gr4vy.paymentServiceDefinitions.list();
67
+ expect(page).toBeDefined();
68
+
69
+ const definition =
70
+ await gr4vy.paymentServiceDefinitions.get("mock-card");
71
+ expect(definition.id).toBe("mock-card");
72
+ });
73
+
74
+ test("create a session for a definition is exercised at the request level", async () => {
75
+ // Session creation is not supported for the mock-card definition.
76
+ await expect(
77
+ gr4vy.paymentServiceDefinitions.session({}, "mock-card")
78
+ ).rejects.toThrow();
79
+ });
80
+
81
+ test("list payment options for a cart", async () => {
82
+ const options = await gr4vy.paymentOptions.list({
83
+ country: "US",
84
+ currency: "USD",
85
+ amount: 1299,
86
+ });
87
+ expect(options).toBeDefined();
88
+ });
89
+
90
+ test("list card scheme definitions", async () => {
91
+ const schemes = await gr4vy.cardSchemeDefinitions.list();
92
+ expect(schemes).toBeDefined();
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,49 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { APPROVING_CARD } 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("Payouts", () => {
13
+ test("listing payouts returns a page", async () => {
14
+ const page = await gr4vy.payouts.list();
15
+ expect(page).toBeDefined();
16
+ });
17
+
18
+ // The mock-card service is not a payout-capable connection, so creating a
19
+ // payout is exercised at the request level — the SDK serialises the payload
20
+ // and the API rejects it for a real reason rather than the test skipping it.
21
+ test("create is exercised at the request level", async () => {
22
+ const service = await gr4vy.paymentServices.create({
23
+ displayName: "Mock card service",
24
+ paymentServiceDefinitionId: "mock-card",
25
+ acceptedCurrencies: ["USD"],
26
+ acceptedCountries: ["US"],
27
+ fields: [{ key: "merchant_id", value: "test" }],
28
+ });
29
+
30
+ await expect(
31
+ gr4vy.payouts.create({
32
+ amount: 1299,
33
+ currency: "USD",
34
+ paymentServiceId: service.id,
35
+ paymentMethod: {
36
+ method: "card",
37
+ number: APPROVING_CARD.number,
38
+ expirationDate: APPROVING_CARD.expiration_date,
39
+ },
40
+ })
41
+ ).rejects.toThrow();
42
+ });
43
+
44
+ test("fetching a missing payout is exercised at the request level", async () => {
45
+ await expect(
46
+ gr4vy.payouts.get("00000000-0000-0000-0000-000000000000")
47
+ ).rejects.toThrow();
48
+ });
49
+ });
@@ -0,0 +1,69 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { ReportCreate } 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 reportCreate = (): ReportCreate => ({
14
+ name: uniqueId("report"),
15
+ schedule: "daily",
16
+ scheduleEnabled: true,
17
+ scheduleTimezone: "UTC",
18
+ spec: {
19
+ model: "transactions",
20
+ params: {
21
+ fields: ["id", "status"],
22
+ filters: { status: ["capture_succeeded"] },
23
+ sort: [{ field: "created_at", order: "desc" }],
24
+ },
25
+ },
26
+ });
27
+
28
+ describe("Reports", () => {
29
+ test("create → get → put (partial) → list", async () => {
30
+ const created = await gr4vy.reports.create(reportCreate());
31
+ expect(created.id).toBeDefined();
32
+
33
+ const fetched = await gr4vy.reports.get(created.id);
34
+ expect(fetched.id).toBe(created.id);
35
+
36
+ // Partial update: toggle the schedule without resending name/spec.
37
+ const updated = await gr4vy.reports.put(
38
+ { scheduleEnabled: false },
39
+ created.id
40
+ );
41
+ expect(updated.scheduleEnabled).toBe(false);
42
+
43
+ const page = await gr4vy.reports.list({});
44
+ expect(page).toBeDefined();
45
+ });
46
+
47
+ test("report executions are listable per-report and account-wide", async () => {
48
+ const created = await gr4vy.reports.create(reportCreate());
49
+
50
+ const perReport = await gr4vy.reports.executions.list(created.id);
51
+ expect(perReport).toBeDefined();
52
+
53
+ const accountWide = await gr4vy.reportExecutions.list({});
54
+ expect(accountWide).toBeDefined();
55
+ });
56
+
57
+ // A freshly created report has no executions yet, so fetching one / generating
58
+ // a download URL is exercised at the request level against a non-existent
59
+ // execution id; the API is expected to reject it.
60
+ test("fetching a missing execution / url is exercised at the request level", async () => {
61
+ const created = await gr4vy.reports.create(reportCreate());
62
+ const bogus = "00000000-0000-0000-0000-000000000000";
63
+
64
+ await expect(gr4vy.reports.executions.get(bogus)).rejects.toThrow();
65
+ await expect(
66
+ gr4vy.reports.executions.url(created.id, bogus)
67
+ ).rejects.toThrow();
68
+ });
69
+ });
@@ -0,0 +1,34 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { ThreeDSecureScenarioCreate } from "../../src/models/components";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ const scenario = (): ThreeDSecureScenarioCreate => ({
13
+ conditions: { amount: 5000 },
14
+ outcome: { authentication: { transactionStatus: "Y" } },
15
+ });
16
+
17
+ describe("3DS Scenarios", () => {
18
+ test("create → list → update (partial) → delete", async () => {
19
+ const created = await gr4vy.threeDsScenarios.create(scenario());
20
+ expect(created.id).toBeDefined();
21
+
22
+ const page = await gr4vy.threeDsScenarios.list();
23
+ expect(page).toBeDefined();
24
+
25
+ // Partial update: replace only the outcome, leaving the conditions intact.
26
+ const updated = await gr4vy.threeDsScenarios.update(
27
+ { outcome: { authentication: { transactionStatus: "N" } } },
28
+ created.id
29
+ );
30
+ expect(updated.id).toBe(created.id);
31
+
32
+ await gr4vy.threeDsScenarios.delete(created.id);
33
+ });
34
+ });
@@ -0,0 +1,74 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import {
4
+ address,
5
+ billingDetails,
6
+ buyer,
7
+ cardPaymentMethod,
8
+ uniqueId,
9
+ } from "../utils/fixtures";
10
+ import { setupMerchant } from "../utils/setup";
11
+
12
+ let gr4vy: Gr4vy;
13
+
14
+ beforeAll(async () => {
15
+ ({ client: gr4vy } = await setupMerchant());
16
+ });
17
+
18
+ describe("Buyer lifecycle", () => {
19
+ test("create buyer, store a card against them, transact, and read it back", async () => {
20
+ // 1. Create a buyer.
21
+ const created = await gr4vy.buyers.create(buyer({ displayName: "Ada Lovelace" }));
22
+ expect(created.id).toBeDefined();
23
+ expect(created.displayName).toBe("Ada Lovelace");
24
+
25
+ // 2. Store a card payment method against the buyer.
26
+ const method = await gr4vy.paymentMethods.create({
27
+ ...cardPaymentMethod(),
28
+ buyerId: created.id,
29
+ });
30
+ expect(method.id).toBeDefined();
31
+
32
+ // 3. The stored method shows up under the buyer's payment methods.
33
+ const buyerMethods = await gr4vy.buyers.paymentMethods.list({
34
+ buyerId: created.id,
35
+ });
36
+ expect(buyerMethods.items.some((m) => m.id === method.id)).toBe(true);
37
+
38
+ // 4. Add shipping details for the buyer.
39
+ const shipping = await gr4vy.buyers.shippingDetails.create(
40
+ {
41
+ firstName: "Ada",
42
+ lastName: "Lovelace",
43
+ address: address(),
44
+ },
45
+ created.id!
46
+ );
47
+ expect(shipping.id).toBeDefined();
48
+
49
+ // 5. Charge the stored method, referencing the buyer + shipping details.
50
+ const transaction = await gr4vy.transactions.create({
51
+ amount: 1299,
52
+ currency: "USD",
53
+ buyerId: created.id,
54
+ shippingDetailsId: shipping.id,
55
+ externalIdentifier: uniqueId("txn"),
56
+ paymentMethod: { method: "id", id: method.id, securityCode: "123" },
57
+ });
58
+ expect(transaction.status).toBe("authorization_succeeded");
59
+
60
+ // 6. Gift cards list for the buyer is reachable (empty is fine).
61
+ const giftCards = await gr4vy.buyers.giftCards.list({ buyerId: created.id });
62
+ expect(Array.isArray(giftCards.items)).toBe(true);
63
+ });
64
+
65
+ test("buyer can be created with billing details and deleted", async () => {
66
+ const created = await gr4vy.buyers.create(
67
+ buyer({ billingDetails: billingDetails({ firstName: "Grace" }) })
68
+ );
69
+ expect(created.billingDetails?.firstName).toBe("Grace");
70
+
71
+ await gr4vy.buyers.delete(created.id!);
72
+ await expect(() => gr4vy.buyers.get(created.id!)).rejects.toThrow();
73
+ });
74
+ });
@@ -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
+ });