-
Notifications
You must be signed in to change notification settings - Fork 109
feat(ack-pay): add approval model types #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
05ddd97
05cd68c
5a358db
77fb9d3
44e73f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@agentcommercekit/ack-pay": patch | ||
| --- | ||
|
|
||
| Add approval request and decision model types. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import * as v from "valibot" | ||
| import { describe, expect, it } from "vitest" | ||
|
|
||
| import * as valibot from "./schemas/valibot" | ||
| import * as zodv3 from "./schemas/zod/v3" | ||
| import * as zodv4 from "./schemas/zod/v4" | ||
|
|
||
| const schemas = { | ||
| valibot: { | ||
| approvalRequest: (value: unknown) => | ||
| v.safeParse(valibot.paymentApprovalRequestSchema, value).success, | ||
| approvalDecision: (value: unknown) => | ||
| v.safeParse(valibot.paymentApprovalDecisionSchema, value).success, | ||
| }, | ||
| zodv3: { | ||
| approvalRequest: (value: unknown) => | ||
| zodv3.paymentApprovalRequestSchema.safeParse(value).success, | ||
| approvalDecision: (value: unknown) => | ||
| zodv3.paymentApprovalDecisionSchema.safeParse(value).success, | ||
| }, | ||
| zodv4: { | ||
| approvalRequest: (value: unknown) => | ||
| zodv4.paymentApprovalRequestSchema.safeParse(value).success, | ||
| approvalDecision: (value: unknown) => | ||
| zodv4.paymentApprovalDecisionSchema.safeParse(value).success, | ||
| }, | ||
| } | ||
|
|
||
| describe.each(Object.entries(schemas))( | ||
| "payment approval schemas (%s)", | ||
| (_name, schema) => { | ||
| it("validates approval requests", () => { | ||
| expect( | ||
| schema.approvalRequest({ | ||
| id: "approval-1", | ||
| paymentRequestId: "payment-request-1", | ||
| paymentOptionId: "usdc-base", | ||
| requesterDid: "did:web:merchant.example", | ||
| reason: "Amount exceeds automated policy limit.", | ||
| expiresAt: "2026-01-01T00:00:00.000Z", | ||
| metadata: { policyId: "spend-limit" }, | ||
| }), | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it("accepts Date timestamps in approval requests", () => { | ||
| expect( | ||
| schema.approvalRequest({ | ||
| id: "approval-1", | ||
| paymentRequestId: "payment-request-1", | ||
| expiresAt: new Date("2026-01-01T00:00:00.000Z"), | ||
| }), | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it("rejects malformed approval requests", () => { | ||
| expect( | ||
| schema.approvalRequest({ | ||
| id: "approval-1", | ||
| requesterDid: "not-a-did", | ||
| }), | ||
| ).toBe(false) | ||
| }) | ||
|
|
||
| it("validates approval decisions", () => { | ||
| expect( | ||
| schema.approvalDecision({ | ||
| requestId: "approval-1", | ||
| decision: "approved", | ||
| approverDid: "did:web:operator.example", | ||
| reason: "Reviewed by operator.", | ||
| decidedAt: "2026-01-01T00:01:00.000Z", | ||
| metadata: { ticketId: "ticket-1" }, | ||
| }), | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it("accepts Date timestamps in approval decisions", () => { | ||
| expect( | ||
| schema.approvalDecision({ | ||
| requestId: "approval-1", | ||
| decision: "approved", | ||
| decidedAt: new Date("2026-01-01T00:01:00.000Z"), | ||
| }), | ||
| ).toBe(true) | ||
| }) | ||
|
|
||
| it("rejects malformed approval decisions", () => { | ||
| expect( | ||
| schema.approvalDecision({ | ||
| requestId: "approval-1", | ||
| decision: "pending", | ||
| decidedAt: "2026-01-01T00:01:00.000Z", | ||
| }), | ||
| ).toBe(false) | ||
| }) | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * A request for human or policy-system approval before a payment is executed. | ||
| * | ||
| * ACK-Pay does not prescribe an approval workflow. This type gives applications | ||
| * and demos a shared object shape for approval-required paths. | ||
| */ | ||
| export interface PaymentApprovalRequest { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because this introduces new exported ACK-Pay model types, the repo’s schema contract requires matching Valibot and Zod v3/v4 schemas for new types. As written, applications receiving these approval objects over the wire can import only TypeScript interfaces, so runtime validation through Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Design question: At this level of optionality, does the type provide enough structure to be useful as a shared contract? Integrators with strict approval requirements (e.g., mandatory Curious if you had specific integration patterns in mind that guided the required vs. optional choices. |
||
| id: string | ||
| paymentRequestId: string | ||
| paymentOptionId?: string | ||
| requesterDid?: string | ||
| reason?: string | ||
| expiresAt?: string | ||
| metadata?: Record<string, unknown> | ||
| } | ||
|
|
||
| export type PaymentApprovalDecisionValue = "approved" | "denied" | ||
|
|
||
| /** | ||
| * The result of a human or policy-system approval request. | ||
| */ | ||
| export interface PaymentApprovalDecision { | ||
| requestId: string | ||
| decision: PaymentApprovalDecisionValue | ||
| approverDid?: string | ||
| reason?: string | ||
| decidedAt: string | ||
| metadata?: Record<string, unknown> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,11 @@ import * as v from "valibot" | |
|
|
||
| const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) | ||
|
|
||
| const timestampSchema = v.pipe( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This refactoring is a nice win regardless of the approval types discussion .. extracting Would you be open to splitting this into its own PR? |
||
| v.union([v.date(), v.string()]), | ||
| v.transform((input) => new Date(input).toISOString()), | ||
| ) | ||
|
|
||
| export const paymentOptionSchema = v.object({ | ||
| id: v.string(), | ||
| amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), | ||
|
|
@@ -19,12 +24,7 @@ export const paymentRequestSchema = v.object({ | |
| id: v.string(), | ||
| description: v.optional(v.string()), | ||
| serviceCallback: v.optional(v.pipe(v.string(), v.url())), | ||
| expiresAt: v.optional( | ||
| v.pipe( | ||
| v.union([v.date(), v.string()]), | ||
| v.transform((input) => new Date(input).toISOString()), | ||
| ), | ||
| ), | ||
| expiresAt: v.optional(timestampSchema), | ||
| paymentOptions: v.pipe( | ||
| v.tupleWithRest([paymentOptionSchema], paymentOptionSchema), | ||
| v.nonEmpty(), | ||
|
|
@@ -36,3 +36,27 @@ export const paymentReceiptClaimSchema = v.object({ | |
| paymentOptionId: v.string(), | ||
| metadata: v.optional(v.record(v.string(), v.unknown())), | ||
| }) | ||
|
|
||
| export const paymentApprovalRequestSchema = v.object({ | ||
| id: v.string(), | ||
| paymentRequestId: v.string(), | ||
| paymentOptionId: v.optional(v.string()), | ||
| requesterDid: v.optional(didUriSchema), | ||
| reason: v.optional(v.string()), | ||
| expiresAt: v.optional(timestampSchema), | ||
| metadata: v.optional(v.record(v.string(), v.unknown())), | ||
| }) | ||
|
|
||
| export const paymentApprovalDecisionValueSchema = v.union([ | ||
| v.literal("approved"), | ||
| v.literal("denied"), | ||
| ]) | ||
|
|
||
| export const paymentApprovalDecisionSchema = v.object({ | ||
| requestId: v.string(), | ||
| decision: paymentApprovalDecisionValueSchema, | ||
| approverDid: v.optional(didUriSchema), | ||
| reason: v.optional(v.string()), | ||
| decidedAt: timestampSchema, | ||
| metadata: v.optional(v.record(v.string(), v.unknown())), | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the line that makes the approval types part of the published API surface. Once released, removing or changing these types is a breaking change. That's the main reason I'd prefer to validate the shapes against a real consumer before committing them here.