From 2926ed9b8915d787566c160d37b6c9509ab4865b Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 19 Oct 2026 14:30:13 +0300 Subject: [PATCH 1/2] build(deps): depend on @norbix.ai/ts 4.29.0 (payment orders + checkout) The payment order hooks wrap api.payments, which the core SDK ships from 4.29.0: devDependency ^4.29.0, peer floor >=4.29.0. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4fc262..e9f2370 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.26.0", + "@norbix.ai/ts": "^4.29.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", @@ -1490,9 +1490,9 @@ } }, "node_modules/@norbix.ai/ts": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.26.0.tgz", - "integrity": "sha512-1dygy1yW+AGOVoEBhu+k501/u/7qIGpZV8gfQchQ3FsfDxc2VBt4Z3oH7Vbh4oI7Tkcc68Gr1atlnM8kGB+gtA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.29.0.tgz", + "integrity": "sha512-3iKNGlLdDjoGhGcdom7Fm6i/8Rna7+oLBaBT4p7dvcYz+iDpvkpzSXpzvDpDkrsfWfWxumwb3nUgMocEmTULVQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index faee0b6..15ff60f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "prepublishOnly": "npm run build" }, "peerDependencies": { - "@norbix.ai/ts": ">=4.26.0", + "@norbix.ai/ts": ">=4.29.0", "@reduxjs/toolkit": ">=2.0.0", "react": ">=18.0.0", "react-redux": ">=9.0.0" @@ -68,7 +68,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.26.0", + "@norbix.ai/ts": "^4.29.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", From 8adc27472795a1d45da11afbe469d8c2a4203534 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 19 Oct 2026 14:32:06 +0300 Subject: [PATCH 2/2] feat(payments): payment order and checkout hooks over api.payments useCreatePaymentOrderMutation, useGetPaymentOrderQuery, useGetPaymentOrdersQuery and useStartPaymentCheckoutMutation, 1:1 with the core SDK 4.29.0 api.payments. New cache tag PaymentOrders: the lists are PaymentOrders/LIST (invalidated by create and checkout), one order is PaymentOrders/ (invalidated by checkout). Tests: each hook reaches its SDK method with the caller args, its kind and cache tags, and it survives the flat buildEndpoints spread. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++ src/createNorbixApi.ts | 1 + src/hooks/api/payments.ts | 77 ++++++++++++++++++++++++++++++ src/hooks/index.ts | 3 ++ tests/payments.test.ts | 99 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/hooks/api/payments.ts diff --git a/README.md b/README.md index 97f8ed5..b90a498 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,10 @@ A _curated_ set of hooks for the most common endpoints. Every hook is end-to-end | `useRegenerateApiKeysMutation` | `api.apikeys.regenerateApiKeys` | invalidates `ApiKey/LIST` | | `useGetPublicFileQuery` | `api.files.getPublicFile` | provides `Files/PUBLIC:` | | `useTestFilesIntegrationApiMutation` | `api.files.testFilesIntegration` | invalidates `Files` + `FilesIntegrations` | +| `useCreatePaymentOrderMutation` | `api.payments.createPaymentOrder` | invalidates `PaymentOrders/LIST` | +| `useGetPaymentOrderQuery` | `api.payments.getPaymentOrder` | provides `PaymentOrders/` | +| `useGetPaymentOrdersQuery` | `api.payments.getPaymentOrders` | provides `PaymentOrders/LIST` | +| `useStartPaymentCheckoutMutation` | `api.payments.startPaymentCheckout` | invalidates `PaymentOrders/LIST` + `PaymentOrders/` | ### Hub surface — `norbix.hub.*` diff --git a/src/createNorbixApi.ts b/src/createNorbixApi.ts index 85acee2..4c923bd 100644 --- a/src/createNorbixApi.ts +++ b/src/createNorbixApi.ts @@ -103,6 +103,7 @@ export function createNorbixApi( 'PaymentPlans', 'PaymentDiscounts', 'PaymentCustomers', + 'PaymentOrders', // AI 'Ai', 'AiChat', diff --git a/src/hooks/api/payments.ts b/src/hooks/api/payments.ts new file mode 100644 index 0000000..bf15c59 --- /dev/null +++ b/src/hooks/api/payments.ts @@ -0,0 +1,77 @@ +// `norbix.api.payments` — payment orders and their checkout (4 endpoints). +// +// The gateway routes behind these hooks, so the endpoint-coverage matrix can +// see them (it looks for the route string in the source). This file never +// builds a URL itself — the core SDK does — so these are documentation: +// +// POST /{version}/payments/orders +// GET /{version}/payments/orders +// GET /{version}/payments/orders/{Id} +// POST /{version}/payments/orders/{Id}/checkout +import type { Norbix } from '@norbix.ai/ts'; + +import type { Builder } from '../index.js'; +import type { Arg, Result } from '../types.js'; + +type CreatePaymentOrder = Norbix['api']['payments']['createPaymentOrder']; +type GetPaymentOrder = Norbix['api']['payments']['getPaymentOrder']; +type GetPaymentOrders = Norbix['api']['payments']['getPaymentOrders']; +type StartPaymentCheckout = Norbix['api']['payments']['startPaymentCheckout']; + +/** + * `api.payments` — 4 endpoints, 1:1 with the core SDK. + * + * Cache: every order list is `PaymentOrders/LIST`, one order is + * `PaymentOrders/`. Creating an order invalidates the lists; starting a + * checkout invalidates the lists and that order (a checkout attempt is added + * to it, and the gateway may move its status). + * + * An order's status also moves when the provider confirms the payment, which + * happens outside this store — refetch (or poll) `useGetPaymentOrderQuery` + * after the buyer comes back on the `returnUrl`. + */ +export const apiPayments = (b: Builder) => ({ + /** + * Creates a payment order: `buyerUserId`, `merchantReference`, `currency`, + * `priceVersion`, `lines` (`sku?`, `title?`, `quantity`, `unitAmountMinor` + * in minor units) and an optional `metadata` string map. Answers `{ id }` + * (`ord_…`). Send an `idempotencyKey` (8+ characters) to make a retry + * safe: the same body with the same key answers the same id. + */ + createPaymentOrder: b.mutation, Arg>({ + query: (args) => (norbix) => norbix.api.payments.createPaymentOrder(args), + invalidatesTags: [{ type: 'PaymentOrders', id: 'LIST' }], + }), + + /** Reads one payment order (`id`). Answers `{ item: PaymentOrderDto }`. */ + getPaymentOrder: b.query, Arg>({ + query: (args) => (norbix) => norbix.api.payments.getPaymentOrder(args), + providesTags: (_res, _err, arg) => [ + { type: 'PaymentOrders' as const, id: arg?.id ?? 'unknown' }, + ], + }), + + /** + * Lists payment orders, filtered by the optional `buyerUserId`, + * `merchantReference` and `status`. Answers `{ list }` (paginated + * `PaymentOrderDto`). + */ + getPaymentOrders: b.query, Arg>({ + query: (args) => (norbix) => norbix.api.payments.getPaymentOrders(args), + providesTags: [{ type: 'PaymentOrders', id: 'LIST' }], + }), + + /** + * Starts (or reuses) a checkout for an order: `id`, `integrationId`, + * `returnUrl`, and optionally `method`, `preferredCountry`, + * `preferredProvider`. Answers `{ checkout, reused, order }` — send the + * buyer to `checkout.paymentUrl`. + */ + startPaymentCheckout: b.mutation, Arg>({ + query: (args) => (norbix) => norbix.api.payments.startPaymentCheckout(args), + invalidatesTags: (_res, _err, arg) => [ + { type: 'PaymentOrders' as const, id: 'LIST' }, + { type: 'PaymentOrders' as const, id: arg?.id ?? 'unknown' }, + ], + }), +}); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 357177e..384f39c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -9,6 +9,7 @@ import { apiAuth } from './api/auth.js'; import { apiDatabase } from './api/database.js'; import { apiFiles } from './api/files.js'; import { apiMembership } from './api/membership.js'; +import { apiPayments } from './api/payments.js'; import { apiPublic } from './api/public.js'; import { apiPush } from './api/push.js'; import { hubAccount } from './hub/account.js'; @@ -103,6 +104,7 @@ export type AllTags = | 'PaymentPlans' | 'PaymentDiscounts' | 'PaymentCustomers' + | 'PaymentOrders' // ---- AI ---- | 'Ai' | 'AiChat' @@ -143,6 +145,7 @@ export function buildEndpoints(builder: Builder) { ...apiDatabase(builder), ...apiApikeys(builder), ...apiFiles(builder), + ...apiPayments(builder), ...apiPublic(builder), ...apiPush(builder), // Hub surface (control plane) diff --git a/tests/payments.test.ts b/tests/payments.test.ts index d63cfa8..7cbe1de 100644 --- a/tests/payments.test.ts +++ b/tests/payments.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; +import { apiPayments } from '../src/hooks/api/payments.js'; import { hubPayments } from '../src/hooks/hub/payments.js'; +import { buildEndpoints } from '../src/hooks/index.js'; /** * `hub.payments` hooks (gateway campaign audit/payments, item 8). Same fake @@ -15,6 +17,8 @@ function fakeBuilder() { } as never; } +type TagFn = (res: unknown, err: unknown, arg: unknown) => unknown; + type Def = { __kind: 'query' | 'mutation'; query: (args: unknown) => (norbix: unknown) => unknown; @@ -33,7 +37,16 @@ function fakeNorbix() { }, }, ); - return { calls, client: { hub: { payments } } }; + const apiPaymentsClient = new Proxy( + {}, + { + get: (_t, name: string) => (args: unknown) => { + calls.push({ method: `api.${name}`, args }); + return Promise.resolve({ ok: true }); + }, + }, + ); + return { calls, client: { hub: { payments }, api: { payments: apiPaymentsClient } } }; } describe('hubPayments', () => { @@ -55,3 +68,87 @@ describe('hubPayments', () => { expect(calls).toEqual([{ method: 'hub.getPaymentsWebhookLog', args }]); }); }); + +/** + * `api.payments` hooks — payment orders + checkout (campaign + * audit/payments-checkout, item sdk-all; core SDK 4.29.0). + */ +describe('apiPayments', () => { + const endpoints = apiPayments(fakeBuilder()) as unknown as Record; + + it('covers the 4 api Payments endpoints of the core SDK', () => { + expect(Object.keys(endpoints).sort()).toEqual( + ['createPaymentOrder', 'getPaymentOrder', 'getPaymentOrders', 'startPaymentCheckout'].sort(), + ); + }); + + it.each([ + [ + 'createPaymentOrder', + 'mutation', + { + buyerUserId: 'usr_1', + merchantReference: 'listing-42', + idempotencyKey: 'listing-42-key', + currency: 'EUR', + priceVersion: 'v1', + lines: [{ sku: 'promo', quantity: 21, unitAmountMinor: 30 }], + }, + ], + ['getPaymentOrder', 'query', { id: 'ord_1' }], + ['getPaymentOrders', 'query', { buyerUserId: 'usr_1', status: 'Paid' }], + [ + 'startPaymentCheckout', + 'mutation', + { id: 'ord_1', integrationId: 'int_montonio', returnUrl: 'https://shop.example/return' }, + ], + ])('%s is a %s that reaches api.payments.%s with the caller args', async (key, kind, args) => { + const { calls, client } = fakeNorbix(); + + await endpoints[key].query(args)(client); + + expect(endpoints[key].__kind).toBe(kind); + expect(calls).toEqual([{ method: `api.${key}`, args }]); + }); + + it('getPaymentOrders provides the order list', () => { + expect(endpoints.getPaymentOrders.providesTags).toEqual([ + { type: 'PaymentOrders', id: 'LIST' }, + ]); + }); + + it('getPaymentOrder provides the order by its id', () => { + const tags = (endpoints.getPaymentOrder.providesTags as TagFn)(undefined, undefined, { + id: 'ord_1', + }); + expect(tags).toEqual([{ type: 'PaymentOrders', id: 'ord_1' }]); + }); + + it('createPaymentOrder invalidates the order lists', () => { + expect(endpoints.createPaymentOrder.invalidatesTags).toEqual([ + { type: 'PaymentOrders', id: 'LIST' }, + ]); + }); + + it('startPaymentCheckout invalidates the order lists and that order', () => { + const tags = (endpoints.startPaymentCheckout.invalidatesTags as TagFn)(undefined, undefined, { + id: 'ord_1', + integrationId: 'int_montonio', + }); + expect(tags).toEqual([ + { type: 'PaymentOrders', id: 'LIST' }, + { type: 'PaymentOrders', id: 'ord_1' }, + ]); + }); + + it('is spread into the full endpoint set, and no Hub hook replaces it', async () => { + const all = buildEndpoints(fakeBuilder()) as unknown as Record; + const { calls, client } = fakeNorbix(); + + for (const key of Object.keys(endpoints)) { + await all[key].query({ id: 'ord_1' })(client); + } + + expect(calls.map((c) => c.method)).toEqual(Object.keys(endpoints).map((k) => `api.${k}`)); + }); +});