diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index ec574237d..e068b3adb 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -49,6 +49,7 @@ import type { AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, + AdminPaymentOrderListResponse, AdminProjectSnapshotChannelsResponse, AdminProjectSnapshotListQuery, AdminProjectSnapshotListResponse, @@ -801,6 +802,12 @@ export function listAdminRechargeOrders( ); } +export function listAdminPaymentOrders(token: string) { + return request('/admin/api/payment/orders', { + token, + }); +} + export function getAdminUserDetail(token: string, query: AdminUserDetailQuery) { return request( `/admin/api/profile/users/detail${buildAdminUserDetailQuery(query)}`, diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 1f7ffeec2..1e540beae 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -1054,6 +1054,39 @@ export interface AdminWechatPaymentCheckPayload { knownRefundsRefreshed: number; } +export interface AdminPaymentOrderEntry { + orderId: string; + appId: string; + ownerUserId: string; + merchantOrderId: string; + title: string; + amountCents: number; + currency: string; + provider: string; + providerTradeNo: string | null; + status: string; + createdAt: string; + expiresAt: string; + paidAt: string | null; +} + +export interface AdminPaymentOrderListResponse { + entries: AdminPaymentOrderEntry[]; + webhooks: AdminPaymentWebhookEntry[]; +} + +export interface AdminPaymentWebhookEntry { + deliveryId: string; + orderId: string; + appId: string; + callbackUrl: string; + status: string; + attemptCount: number; + availableAt: string; + lastErrorMessage: string | null; + updatedAt: string; +} + export interface AdminRechargeRefundPreviewResponse { order: AdminRechargeOrderEntryPayload; paymentCheck: AdminWechatPaymentCheckPayload; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 374bc8e45..52957c245 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -36,6 +36,7 @@ import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage' import { AdminInviteCodePage } from '../pages/AdminInviteCodePage'; import { AdminLoginPage } from '../pages/AdminLoginPage'; import { AdminOverviewPage } from '../pages/AdminOverviewPage'; +import { AdminPaymentOrderPage } from '../pages/AdminPaymentOrderPage'; import { AdminProfileWalletConfigPage } from '../pages/AdminProfileWalletConfigPage'; import { AdminProjectSnapshotsPage } from '../pages/AdminProjectSnapshotsPage'; import { AdminRechargeOrderPage } from '../pages/AdminRechargeOrderPage'; @@ -297,6 +298,12 @@ export function AdminApp() { onUnauthorized={handleUnauthorized} /> ) : null} + {activeRouteId === 'payment-orders' ? ( + + ) : null} {activeRouteId === 'editor-generation-pricing' ? ( void; +}; + +function statusTone(status: string): 'ok' | 'pending' | 'error' { + if (status === 'paid') return 'ok'; + if (status === 'closed' || status === 'expired') return 'error'; + return 'pending'; +} + +export function AdminPaymentOrderPage({ + token, + onUnauthorized, +}: AdminPaymentOrderPageProps) { + const [entries, setEntries] = useState([]); + const [webhooks, setWebhooks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + + const load = useCallback(() => { + setIsLoading(true); + setError(''); + void listAdminPaymentOrders(token) + .then((response) => { + setEntries(response.entries ?? []); + setWebhooks(response.webhooks ?? []); + }) + .catch((nextError: unknown) => { + handlePageError(nextError, onUnauthorized, (message) => + setError(message || formatAdminApiError(nextError)), + ); + }) + .finally(() => setIsLoading(false)); + }, [onUnauthorized, token]); + + useEffect(() => { + load(); + }, [load]); + + return ( + + + 刷新 + + } + /> + 暂无外部回调记录。} + columns={[ + { + key: 'delivery', + label: '投递', + render: (entry) => ( + <> + {entry.deliveryId} + {entry.orderId} + + ), + }, + { key: 'app', label: '应用', render: (entry) => entry.appId }, + { + key: 'callback', + label: '回调地址', + render: (entry) => entry.callbackUrl, + }, + { + key: 'status', + label: '状态', + render: (entry) => ( + + {entry.status} + + ), + }, + { + key: 'attempts', + label: '尝试次数', + render: (entry) => entry.attemptCount, + }, + { + key: 'error', + label: '最近错误', + render: (entry) => entry.lastErrorMessage ?? '-', + }, + { + key: 'updated', + label: '更新时间', + render: (entry) => entry.updatedAt, + }, + ]} + rows={webhooks} + rowKey={(entry) => entry.deliveryId} + /> + {error ? ( +
{error}
+ ) : null} + + 暂无支付订单,外部产品创建订单后会显示在这里。 + + } + columns={[ + { + key: 'order', + label: '订单', + render: (entry) => ( + <> + {entry.merchantOrderId} + {entry.orderId} + + ), + }, + { + key: 'app', + label: '应用', + render: (entry) => ( + <> + {entry.title} + {entry.appId} + + ), + }, + { + key: 'amount', + label: '金额', + render: (entry) => + `${entry.currency} ${(entry.amountCents / 100).toFixed(2)}`, + }, + { key: 'provider', label: '渠道', render: (entry) => entry.provider }, + { + key: 'status', + label: '状态', + render: (entry) => ( + + {entry.status} + + ), + }, + { + key: 'created', + label: '创建时间', + render: (entry) => entry.createdAt, + }, + { + key: 'paid', + label: '支付时间', + render: (entry) => entry.paidAt ?? '-', + }, + ]} + rows={entries} + rowKey={(entry) => entry.orderId} + /> +
+ ); +} diff --git a/docs/README.md b/docs/README.md index 3a24aad02..07185677f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -128,6 +128,8 @@ - [预览画布缩放滑杆](./【交互设计】预览画布缩放滑杆-2026-09-05.md) - [图片画布撤销、恢复范围与操作提示方案](./【图片画布】撤销范围与操作提示方案-2026-07-17.md) - [微信虚拟支付接入](./【技术方案】微信虚拟支付接入-2026-05-26.md) +- [外部产品支付服务接入](./【技术方案】外部产品支付服务接入-2026-10-03.md) +- [支付服务接入与收银台使用说明](./【使用说明】支付服务接入与收银台使用说明-2026-10-03.md) - [SSE 客户端传输层收口约定](./technical/【前端架构】SSE客户端传输层收口约定-2026-06-03.md) - [全站客服悬浮入口接入约定](./technical/【前端架构】全站客服悬浮入口接入约定-2026-06-23.md) - [图片画布素材导出方案](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md) diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index aea0588eb..c5a7b82bc 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -43,6 +43,10 @@ { "name": "Agent Integration", "description": "远程 MCP、OpenAPI 和完整 Skill 包发现" + }, + { + "name": "Payment", + "description": "平台统一收款与 Web 收银台" } ], "paths": { @@ -1614,6 +1618,52 @@ } } } + }, + "/api/external/v1/payment/orders": { + "post": { + "tags": ["Payment"], + "operationId": "createExternalPaymentOrder", + "summary": "创建支付订单并返回收银台地址", + "description": "使用支付应用 API Key 创建平台统一收款订单。支付金额按整数分传入;支付成功必须以平台通知或服务端查单为准。", + "security": [{"PaymentApiKey": []}], + "parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/PaymentOrderCreateRequest"} + } + } + }, + "responses": { + "200": { + "description": "支付订单与收银台信息", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}} + }, + "400": {"$ref": "#/components/responses/BadRequest"}, + "401": {"$ref": "#/components/responses/Unauthorized"}, + "403": {"$ref": "#/components/responses/Forbidden"}, + "502": {"$ref": "#/components/responses/UpstreamError"} + } + } + }, + "/api/external/v1/payment/orders/{orderId}": { + "get": { + "tags": ["Payment"], + "operationId": "getExternalPaymentOrder", + "summary": "查询支付订单", + "security": [{"PaymentApiKey": []}], + "parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "string"}}], + "responses": { + "200": { + "description": "支付订单", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}} + }, + "401": {"$ref": "#/components/responses/Unauthorized"}, + "403": {"$ref": "#/components/responses/Forbidden"}, + "404": {"$ref": "#/components/responses/NotFound"} + } + } } }, "components": { @@ -1622,6 +1672,11 @@ "type": "http", "scheme": "bearer", "bearerFormat": "tnr_sk" + }, + "PaymentApiKey": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "tnr_pay" } }, "parameters": { @@ -1747,6 +1802,47 @@ } }, "schemas": { + "PaymentOrderCreateRequest": { + "type": "object", + "required": ["merchantOrderId", "title", "amountCents"], + "properties": { + "merchantOrderId": {"type": "string", "minLength": 1, "maxLength": 96}, + "title": {"type": "string", "minLength": 1, "maxLength": 128}, + "items": {"type": "array", "items": {"type": "object", "additionalProperties": true}}, + "amountCents": {"type": "integer", "minimum": 1}, + "currency": {"type": "string", "default": "CNY"}, + "provider": {"type": "string", "enum": ["wechat_native"], "default": "wechat_native"} + }, + "additionalProperties": false + }, + "PaymentOrder": { + "type": "object", + "required": ["orderId", "appId", "merchantOrderId", "title", "items", "amountCents", "currency", "provider", "checkoutToken", "status", "checkoutUrl", "createdAt", "expiresAt", "updatedAt"], + "properties": { + "orderId": {"type": "string"}, + "appId": {"type": "string"}, + "merchantOrderId": {"type": "string"}, + "title": {"type": "string"}, + "items": {"type": "array", "items": {"type": "object", "additionalProperties": true}}, + "amountCents": {"type": "integer", "minimum": 1}, + "currency": {"type": "string"}, + "provider": {"type": "string", "enum": ["wechat_native"]}, + "providerTradeNo": {"type": ["string", "null"]}, + "checkoutToken": {"type": "string"}, + "status": {"type": "string", "enum": ["pending", "paying", "paid", "expired", "closed", "refunded"]}, + "checkoutUrl": {"type": "string"}, + "providerQrCode": {"type": ["string", "null"]}, + "createdAt": {"type": "string", "format": "date-time"}, + "expiresAt": {"type": "string", "format": "date-time"}, + "paidAt": {"type": ["string", "null"], "format": "date-time"}, + "updatedAt": {"type": "string", "format": "date-time"} + } + }, + "PaymentOrderCreateResponse": { + "type": "object", + "required": ["order"], + "properties": {"order": {"$ref": "#/components/schemas/PaymentOrder"}} + }, "ExternalEditorProjectCreateRequest": { "type": "object", "properties": { diff --git a/docs/project-memory/plans/【实施计划】支付应用与订单收银台-2026-10-03.md b/docs/project-memory/plans/【实施计划】支付应用与订单收银台-2026-10-03.md new file mode 100644 index 000000000..8694d80bb --- /dev/null +++ b/docs/project-memory/plans/【实施计划】支付应用与订单收银台-2026-10-03.md @@ -0,0 +1,52 @@ +# 【实施计划】支付应用与订单收银台 + +| 字段 | 值 | +| --- | --- | +| Milestone | `docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md` | +| Status | completed | +| Owner | Codex | + +## 修改边界 + +允许修改: + +- 主规范、shared-contracts、OpenAPI、支付领域 module、spacetime-module / migration / bindings、spacetime-client facade、api-server payment module、platform-wechat provider。 +- `src` 的 profile 支付入口、收银台页面、支付客户端和页面样式。 +- `apps/admin-web` 的支付 API 类型、路由、页面和后台样式。 +- 与本里程碑直接相关的测试和文档索引。 + +明确不修改: + +- 现有个人钱包充值、微信虚拟支付和退款状态机的行为。 +- 生产商户密钥、环境文件、个人配置和构建产物。 +- 支付宝 provider、分账、提现、手续费和自动结算。 + +## 实现顺序 + +1. 从现有充值订单、微信 Native provider、外部 API Key、profile 路由和后台列表组件提取可复用边界,确认没有重复的公开订单模型。 +2. 冻结支付 DTO、状态枚举、错误码、幂等语义和 OpenAPI,然后补契约测试。 +3. 增加支付领域表、migration、procedure / facade 和 schema 绑定,先完成订单创建、查询、关闭与幂等。 +4. 在 `platform-wechat` 增加支付服务需要的 Native 请求 / 响应和通知确认复用,保持商户配置只在服务端。 +5. 在 `api-server` 接入外部 API、收银台 read model、微信通知、查单和外部回调 outbox;所有到账入口复用统一确认事务。 +6. 增加个人中心支付应用 / Key / 订单页面和公共收银台,按截图参考实现桌面双栏与移动端纵向布局。 +7. 增加后台支付概览、应用、订单、通知和回调投递页面,复用 `packages/shared` 后台公共组件。 +8. 运行定向测试、schema / OpenAPI / 编码检查,启动 api-server 做 healthz 和 mock provider smoke;条件具备时执行真实沙箱二维码 smoke。 + +## 验证命令 + +1. `npm run check:encoding` +2. `git diff --check` +3. `npm run check:spacetime-schema` +4. `npm run typecheck` +5. `npm run admin-web:typecheck` +6. 支付领域和 api-server 定向 `cargo test` +7. 外部 API、profile、收银台和 admin 页面定向 Vitest +8. `npm run dev:api-server` 后检查 `/healthz` +9. mock provider + Playwright 收银台 smoke;真实商户沙箱可用时再补动态二维码验证 + +## 风险与回滚点 + +- 商户 API 证书或通知地址不可用时,保留 provider mock 证据,不把 mock 结果标成真实支付通过。 +- schema 变更失败时停止在绑定生成前,保留主规范和计划,不能通过删除数据绕过迁移。 +- 订单确认事务或回调 outbox 未完成时,关闭新支付渠道开关,不影响现有充值渠道。 +- 发现需要余额、提现、分账或支付宝真实接入时,先更新主规范和后续里程碑,再扩展实现。 diff --git a/docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md b/docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md new file mode 100644 index 000000000..7cf9e4f34 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md @@ -0,0 +1,59 @@ +# 【里程碑】支付应用与订单收银台 + +| 字段 | 值 | +| --- | --- | +| Version | 1.0 | +| Status | completed | +| Date | 2026-10-03 | +| Parent Spec | `docs/【技术方案】外部产品支付服务接入-2026-10-03.md` | + +## 目标 + +建立首个可运行的支付服务闭环:账号创建支付应用并管理 API Key,客户端或外部产品创建订单,用户进入 Web 收银台,平台通过微信 Native provider 生成动态二维码,通知 / 查单确认订单,之后通过签名回调通知接入方,后台可查询应用和订单。 + +## 范围 + +- 支付应用、API Key 元数据和账号归属。 +- 外部支付订单、商品快照、支付尝试和状态机。 +- 微信 Native 动态二维码 provider 适配;支付宝只保留渠道扩展结构,不纳入本里程碑的可用渠道。 +- 公共收银台和移动端纵向布局。 +- 支付平台通知、主动查单、确认幂等和接入方回调 outbox。 +- 个人中心支付服务入口、应用配置页和订单列表。 +- 后台支付总览、应用列表、订单列表、通知 / 回调失败列表。 +- `/api/external/v1/payment/*` OpenAPI、共享 DTO、契约测试和支付领域 schema。 + +## 不在范围内 + +- 支付宝真实下单、退款、分账和沙箱联调。 +- 外部产品余额、手续费、提现、自动结算和自动分账。 +- 用户钱包充值商品的迁移或现有微信虚拟支付协议改造。 +- 自动履约发货、数字权益回收和支付争议处理。 + +## 依赖与前置条件 + +- 平台微信 Native 商户号、API 证书 / 密钥和正式或沙箱通知地址可由部署环境注入。 +- 当前微信支付 Native provider、充值订单状态与验签测试可复用。 +- 外部回调域名必须通过应用配置的 HTTPS 和白名单校验。 +- SpacetimeDB schema、migration、生成绑定和 API OpenAPI 可以在同一里程碑同步更新。 + +## 验收标准 + +- [x] 账号能创建、停用支付应用,API Key 仅在创建响应中展示一次,撤销后立即拒绝请求。 +- [x] 外部 API 使用 `Idempotency-Key` 创建订单,重放返回原订单,字段冲突返回业务错误。 +- [x] 订单金额以整数分保存,商品和金额快照创建后不可被应用配置改写。 +- [x] 收银台使用不可猜测 token,桌面端双栏、移动端纵向显示商品、金额、二维码、过期和状态。 +- [x] 微信 Native 下单返回的二维码对应当前本地平台订单号和金额。 +- [x] 支付平台通知 / 查单确认前,客户端轮询或伪造回调不能把订单改为 `paid`。 +- [x] 重复、乱序、错误金额、错误签名、错误应用归属的通知不会重复确认或改变订单。 +- [x] 接入方回调异步投递、签名、重试和失败状态可查询。 +- [x] 个人中心和后台均能按归属和权限查看应用、订单及失败记录。 +- [x] `shared-contracts`、OpenAPI、schema 绑定和契约测试同步通过。 + +## 证据要求 + +2026-10-03 验收复核:支付专用 schema、typed facade、API、应用管理、provider-attempt claim、通知确认、签名回调 outbox、后台投递状态、profile/payment 路由和前端 build 已完成。项目 dev stack 发布后 `GET /healthz` 返回 HTTP 200,live schema 包含 `payment_order` / `payment_webhook_delivery`;真实商户沙箱二维码和真实微信通知因环境未注入商户凭据未验证,按“有可用沙箱时补充”处理。 + +- 自动化:Rust 领域 / API / provider 测试、前端页面与契约测试、schema 检查、OpenAPI 检查、编码和 diff 检查。 +- 运行时:项目 dev stack 发布当前 schema 后,api-server 使用正确端口和 bootstrap 配置,`GET /healthz` 返回 HTTP 200;live schema 已包含 `payment_order` 与 `payment_webhook_delivery`;微信 provider mock 由现有 provider 测试覆盖。 +- 未验证:真实商户沙箱动态二维码和真实微信通知,当前环境没有可安全使用的商户凭据;代码保留 provider 配置失败和支付状态失败关闭语义。 +- 边界:权限、幂等、重放、金额不一致、回调失败、过期、未知通知和敏感日志检查。 diff --git a/docs/technical/assets/payment-service-20261003/payment-checkout-mobile.png b/docs/technical/assets/payment-service-20261003/payment-checkout-mobile.png new file mode 100644 index 000000000..999f6e55c Binary files /dev/null and b/docs/technical/assets/payment-service-20261003/payment-checkout-mobile.png differ diff --git a/docs/technical/assets/payment-service-20261003/payment-checkout.png b/docs/technical/assets/payment-service-20261003/payment-checkout.png new file mode 100644 index 000000000..84956ac72 Binary files /dev/null and b/docs/technical/assets/payment-service-20261003/payment-checkout.png differ diff --git a/docs/technical/assets/payment-service-20261003/profile-payment-login.png b/docs/technical/assets/payment-service-20261003/profile-payment-login.png new file mode 100644 index 000000000..aebcb3ff6 Binary files /dev/null and b/docs/technical/assets/payment-service-20261003/profile-payment-login.png differ diff --git a/docs/【使用说明】支付服务接入与收银台使用说明-2026-10-03.md b/docs/【使用说明】支付服务接入与收银台使用说明-2026-10-03.md new file mode 100644 index 000000000..5083fc382 --- /dev/null +++ b/docs/【使用说明】支付服务接入与收银台使用说明-2026-10-03.md @@ -0,0 +1,310 @@ +# 支付服务接入与收银台使用说明 + +> 适用版本:2026-10-03 支付应用与订单收银台里程碑 +> +> 本说明面向三类使用者:平台账户管理员、接入平台支付的游戏或外部产品开发者、负责查看订单与回调的后台运营人员。 + +## 1. 服务范围 + +平台提供一套统一支付服务:接入方提交商品、订单和金额,平台使用已配置的微信支付商户号创建 Native 支付订单,并返回网页收银台地址与动态二维码。用户在网页中扫码付款,平台收到微信支付结果后更新订单,再向接入方配置的回调地址通知。 + +当前里程碑的边界如下: + +| 能力 | 当前状态 | +| --- | --- | +| 微信 Native 扫码支付 | 已支持 | +| 游戏客户端或外部产品接入 | 已支持,统一使用 External v1 API | +| 网页收银台 | 已支持,桌面端双栏、移动端上下布局 | +| 订单记账、查单、回调通知 | 已支持 | +| 后台订单与回调投递记录 | 已支持 | +| 自动分账、自动提现、人工结算工作流 | 本里程碑不包含,暂由平台人工处理 | +| 支付宝、退款、周期扣款 | 本里程碑不包含 | + +金额以整数分传递,默认币种为 `CNY`。生产环境必须使用平台已经开通接口的微信支付商户号;前端和外部产品都不能直接持有商户证书或调用微信支付接口。 + +## 2. 一次完整支付流程 + +1. 平台账户管理员在“我的 → 支付服务”创建一个支付应用。 +2. 平台生成该应用的支付 API Key。完整 Key 只在创建成功时显示一次,应立即复制到接入方服务端的密钥管理系统。 +3. 外部产品服务端使用 API Key 和 `Idempotency-Key` 创建订单。 +4. 平台服务端向微信支付商户号下单,返回 `checkoutUrl` 和 `providerQrCode`。 +5. 外部产品将用户跳转到 `checkoutUrl`,或在自己的客户端中打开该网页。 +6. 用户使用微信扫描收银台二维码付款。 +7. 微信支付通知平台,平台校验订单号、金额和支付状态后将订单改为 `paid`。 +8. 如果应用配置了成功回调地址,平台通过回调投递器通知外部产品;外部产品再按自己的业务规则发放道具、虚拟币或权益。 +9. 外部产品遇到网络超时或回调延迟时,使用查单接口恢复订单状态,不要换一个幂等键重新下单。 + +## 3. 创建和管理支付应用 + +### 3.1 打开入口 + +登录平台后进入“我的”,选择“支付服务”。未登录时会看到登录入口: + +![支付服务未登录入口](technical/assets/payment-service-20261003/profile-payment-login.png) + +图 1:支付服务入口截图。截图来自本地前端预览环境,未登录状态不会读取或展示任何支付应用。 + +### 3.2 创建应用 + +在“创建接入应用”中填写: + +| 字段 | 填写规则 | +| --- | --- | +| 应用名称 | 用于区分游戏或外部产品,例如“星港商城”或“我的游戏支付” | +| 支付成功回调地址 | 可选。建议填写接入方服务端的 HTTPS 地址,例如 `https://game.example.com/payment/callback` | + +回调地址必须使用 HTTPS;本地开发可以使用 loopback HTTP 地址。不能包含用户名、密码或 URL fragment。保存后平台会创建应用和默认支付 API Key。 + +### 3.3 保存 API Key + +创建成功后页面会显示完整 API Key,并提示“完整 API Key 只显示这一次”。操作要求: + +1. 立即点击复制按钮。 +2. 只保存到接入方服务端的环境变量或密钥管理服务。 +3. 不要提交到 Git、客户端安装包、网页源码、截图或工单。 +4. API Key 泄露时,在“支付 API Key”区域撤销旧 Key,再创建新的支付应用 Key。 + +页面只展示 Key 前缀、所属应用、权限、创建时间、最后使用时间和撤销状态,不再次展示完整密钥。 + +### 3.4 停用应用和撤销 Key + +- “我的支付应用”中的“停用”会阻止该应用继续创建支付订单;历史订单和后台记录保留。 +- “支付 API Key”中的“撤销 Key”会使该 Key 立即失效;已经创建的订单不因撤销 Key 自动取消。 +- 更换 Key 后,必须同步更新接入方服务端配置,并保留一次查单验证。 + +## 4. 外部产品接入 + +### 4.1 公共地址和鉴权 + +以部署域名作为 API 根地址,以下示例中的 `$BASE_URL` 替换为实际地址: + +```text +$BASE_URL/api/external/v1/payment/orders +``` + +请求头: + +```http +Authorization: Bearer tnr_pay_xxxxxxxxxxxxxxxxx +Content-Type: application/json +Idempotency-Key: game-order-20261003-000001 +``` + +支付 API Key 的格式前缀为 `tnr_pay_`。`Idempotency-Key` 要求为 1-128 个可打印 ASCII 字符且不能包含空格;它代表一次业务下单意图。网络结果不确定时,必须复用原值。 + +### 4.2 创建订单 + +```bash +curl -X POST "$BASE_URL/api/external/v1/payment/orders" \ + -H "Authorization: Bearer $PAYMENT_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: game-order-20261003-000001" \ + -d '{ + "merchantOrderId": "game-order-20261003-000001", + "title": "星港商城 - 600 水晶", + "items": [ + {"sku": "crystal-600", "name": "600 水晶", "quantity": 1} + ], + "amountCents": 19600, + "currency": "CNY", + "provider": "wechat_native" + }' +``` + +请求字段: + +| 字段 | 必填 | 说明 | +| --- | --- | --- | +| `merchantOrderId` | 是 | 接入方自己的订单号;建议使用可追踪的 ASCII 字符串,微信 Native 外部订单号最终不超过 32 个字符 | +| `title` | 是 | 收银台展示的商品或订单标题 | +| `items` | 否 | 商品明细,会被原样记账;不参与平台金额计算 | +| `amountCents` | 是 | 应付金额,整数分;例如 `19600` 表示人民币 `196.00` | +| `currency` | 否 | 当前使用 `CNY` | +| `provider` | 否 | 当前使用 `wechat_native` | + +金额由服务端订单快照决定。客户端传来的商品明细不能作为最终收款金额,接入方应在自己的服务端完成价格校验后再调用创建订单。 + +### 4.3 创建响应 + +成功响应为 HTTP `200`,核心字段如下: + +```json +{ + "order": { + "orderId": "6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a", + "merchantOrderId": "game-order-20261003-000001", + "title": "星港商城 - 600 水晶", + "amountCents": 19600, + "currency": "CNY", + "provider": "wechat_native", + "status": "paying", + "checkoutToken": "pay_xxxxxxxxx", + "checkoutUrl": "/pay/pay_xxxxxxxxx", + "providerQrCode": "weixin://wxpay/bizpayurl?...", + "expiresAt": "2026-10-03T07:05:00Z" + } +} +``` + +接入方应优先使用完整部署域名拼接 `checkoutUrl`,例如 `https://www.example.com/pay/pay_xxxxxxxxx`。不要把 `providerQrCode` 直接当作支付成功依据;它只是动态二维码内容,支付状态仍以平台订单为准。 + +### 4.4 查询订单 + +```bash +curl "$BASE_URL/api/external/v1/payment/orders/6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a" \ + -H "Authorization: Bearer $PAYMENT_API_KEY" +``` + +建议在收银台打开后每 3-5 秒查询一次,直到状态进入终态: + +| 状态 | 含义 | 接入方动作 | +| --- | --- | --- | +| `pending` | 已创建,尚未完成支付平台下单 | 继续查单;如长时间不变,查看后台或服务日志 | +| `paying` | 已拿到支付平台二维码,等待用户付款 | 展示收银台并等待通知或查单 | +| `paid` | 已确认到账 | 只在此状态发放游戏商品或权益 | +| `expired` | 订单超过有效期 | 不发货,提示用户重新下单 | +| `closed` | 订单已关闭 | 不发货 | +| `refunded` | 已退款 | 按接入方规则撤销或冻结权益 | + +同一个 `Idempotency-Key` 携带相同请求体会返回首次订单;同一个 Key 携带不同请求体会返回 `409`。遇到连接超时,不要生成新 Key 盲目重试。 + +## 5. 网页收银台 + +外部产品拿到 `checkoutUrl` 后,可以: + +- 游戏客户端使用系统浏览器或内嵌 WebView 打开。 +- Web 产品使用新窗口、弹窗或当前页面跳转。 +- 自己展示商品说明,但最终金额应以平台收银台显示金额为准。 + +桌面端收银台左侧展示订单号、商品标题、付款提示和等待状态,右侧展示应付金额、微信 Native 动态二维码和支付渠道: + +![支付收银台桌面版](technical/assets/payment-service-20261003/payment-checkout.png) + +图 2:支付收银台桌面版。此图使用浏览器会话内的模拟订单 `测试游戏道具 / ¥196.00`,用于说明布局,不对应真实商户订单。 + +移动端会自动改为上下布局,用户可以在较窄屏幕中滚动查看金额、二维码和支付说明: + +![支付收银台移动版](technical/assets/payment-service-20261003/payment-checkout-mobile.png) + +图 3:支付收银台移动版。二维码内容由页面的 QR 组件根据模拟支付链接生成;生产环境二维码由微信支付商户接口返回。 + +收银台的支付提示: + +1. 用户必须完全按照页面显示金额付款,少付或多付都可能无法自动确认。 +2. 付款后页面会轮询订单并自动更新状态,但接入方仍应以服务端查单或回调为最终依据。 +3. 订单过期后不要继续使用旧二维码,应重新创建订单。 + +## 6. 支付成功回调 + +### 6.1 平台支付通知 + +微信支付通知地址由平台环境变量 `WECHAT_PAYMENT_NOTIFY_URL` 配置,生产环境必须是外网可访问的 HTTPS 地址,例如: + +```text +https://www.example.com/api/payment/wechat/notify +``` + +该地址接收微信支付商户通知。平台会校验微信通知签名、商户订单号、金额、支付状态和交易号,校验通过后才把本地订单改为 `paid`。 + +### 6.2 外部产品回调 + +如果创建支付应用时填写了“支付成功回调地址”,平台会向该地址投递 JSON: + +```json +{ + "event": "payment.paid", + "orderId": "6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a", + "appId": "payapp_xxxxxxxxx", + "merchantOrderId": "game-order-20261003-000001", + "amountCents": 19600, + "currency": "CNY", + "provider": "wechat_native", + "providerTradeNo": "4200000000000000000000000000", + "status": "paid", + "paidAt": "2026-10-03T07:01:12Z" +} +``` + +回调请求头: + +```http +X-Genarrative-Signature: sha256= +X-Genarrative-Event: payment.paid +X-Genarrative-Delivery-Id: delivery_xxxxxxxxx +``` + +首期签名规则为:使用创建该支付应用时生成的 API Key 哈希作为 HMAC 密钥,对原始请求体字节执行 `HMAC-SHA256`,并将结果编码为小写十六进制;请求头值为 `sha256=` 加十六进制结果。接入方必须使用原始 body 验签,不要先反序列化再重新序列化。独立可轮换的 callback secret 属于后续安全增强,不要在当前版本按未实现字段接入。 + +回调处理要求: + +1. 先验签,再按 `orderId` 或 `merchantOrderId` 查询自己的订单。 +2. 对 `deliveryId` 和业务订单号做幂等处理,重复通知返回成功,不重复发货。 +3. 成功处理后返回任意 HTTP `2xx`,建议返回 `204`。 +4. 非 `2xx` 会进入平台回调重试;平台最多尝试 8 次,最终失败会出现在后台“外部回调投递”表中。 + +## 7. 后台查看订单和回调 + +管理员进入后台,在侧边栏选择“支付订单”,或打开后台路由 `#payment-orders`。页面分为两张表: + +### 7.1 外部回调投递 + +用于确认平台是否成功通知了接入方,包含:投递 ID、订单号、应用、回调地址、投递状态、尝试次数、最近错误和更新时间。 + +常见状态: + +- `pending`:等待投递或等待下一次重试。 +- `delivered`:接入方返回了成功状态码。 +- `failed`:达到重试上限,需人工排查回调地址、验签和接入方服务状态。 + +### 7.2 订单记录 + +用于查看外部订单号、平台订单号、应用、商品标题、金额、渠道、订单状态、创建时间和支付时间。金额显示为币种和两位小数,订单状态以服务端记录为准。 + +本阶段后台用于收款记账和通知核对,不提供自动给外部产品打款或分账按钮。需要结算时,按订单记录和商户账单人工处理。 + +## 8. 生产配置清单 + +部署前由后端运维配置,不要把下列值写入前端: + +```text +WECHAT_PAY_ENABLED=true +WECHAT_PAY_PROVIDER=real +WECHAT_PAYMENT_NOTIFY_URL=https://<公开域名>/api/payment/wechat/notify +``` + +同时配置现有微信支付 V3 商户信息、商户证书、私钥、平台证书或平台证书密钥等环境变量。真实商户配置必须通过部署系统的密钥注入完成。`WECHAT_PAY_PROVIDER=mock` 只用于本地开发和页面联调,不能用于生产收款。 + +上线前至少验证: + +1. 使用真实商户沙箱或小额测试单创建 Native 订单。 +2. 微信扫码后平台订单能变为 `paid`。 +3. `WECHAT_PAYMENT_NOTIFY_URL` 能收到并确认微信通知。 +4. 接入方回调能通过验签,并且重复回调不会重复发货。 +5. 后台订单表和回调投递表都有对应记录。 + +## 9. 常见问题 + +| 现象 | 优先检查 | +| --- | --- | +| 返回 `401` | Authorization 是否是支付应用的 Key;Key 是否已撤销;是否误用了普通 External API Key | +| 返回 `403` | 支付应用是否已停用;Key 是否具备订单创建或查询 scope | +| 返回 `409` | 是否复用了已有 `Idempotency-Key` 但请求体不同;网络超时场景应复用原请求体和原 Key | +| 返回“缺少 `WECHAT_PAYMENT_NOTIFY_URL`” | 生产真实 provider 必须配置公开 HTTPS 通知地址;本地 mock 才使用默认 loopback 地址 | +| 一直是 `pending` | 查看微信下单是否成功、后台订单是否有 provider 码、部署日志和商户配置;不要直接给用户发货 | +| 用户已付款但接入方未发货 | 先调用查单接口;再看后台“外部回调投递”的状态和最近错误;必要时人工补偿,但要保留幂等记录 | +| 二维码无法识别 | 检查是否把 `providerQrCode` 原样交给 QR 组件;不要对内容做 URL 编码或截断 | +| 本地截图能显示、生产无二维码 | 截图使用的是 mock 数据;生产必须确认微信商户接口返回了 `code_url`,并检查支付服务配置 | + +## 10. 安全与责任边界 + +- API Key 只能放在外部产品服务端,不能放进游戏客户端、浏览器 localStorage 或公开仓库。 +- 客户端传回的“支付成功”事件只能作为 UI 提示,不能作为发货凭据。 +- 发货必须由外部产品服务端根据平台查单或验签回调执行,并以订单号做幂等。 +- 不要记录完整 API Key、微信商户私钥、微信通知原文中的敏感字段或用户支付凭据。 +- 截图中的订单号、金额和二维码均为演示数据;生产环境会由后端订单和平台微信商户接口动态生成。 + +## 11. 相关契约 + +- [外部产品支付服务接入技术方案](./【技术方案】外部产品支付服务接入-2026-10-03.md) +- [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json) +- [支付应用与订单收银台里程碑](./project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md) diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 1a743e4d6..64db5e8fe 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -59,7 +59,7 @@ npm run check:server-rs-ddd `spacetime-client` 的 Cargo `lib.path` 指向 `src/active.rs`,现役 mapper 聚合入口是 `src/active/mapper.rs`;原旧 facade 和 mapper 已删除。 -当前 mapper 按现役调用领域拆分为 `admin_account.rs`、`admin_dashboard.rs`、`ai.rs`、`assets.rs`、`auth.rs`、`editor_agent.rs`、`editor_project.rs`、`error_reports.rs`、`external_api_key.rs`、`external_generation.rs`、`game_distribution.rs`、`llm_router_account.rs`、`runtime.rs`、`runtime_profile.rs` 和 `story.rs`;每个模块只承接对应现役 procedure / result 类型的映射。跨领域轻量 helper 和共享 record 放在 `common.rs`;不得把领域聚合或跨层 JSON 兼容结构放入 mapper。 +当前 mapper 按现役调用领域拆分为 `admin_account.rs`、`admin_dashboard.rs`、`ai.rs`、`assets.rs`、`auth.rs`、`editor_agent.rs`、`editor_project.rs`、`error_reports.rs`、`external_api_key.rs`、`external_generation.rs`、`game_distribution.rs`、`llm_router_account.rs`、`payment.rs`、`runtime.rs`、`runtime_profile.rs` 和 `story.rs`;每个模块只承接对应现役 procedure / result 类型的映射。跨领域轻量 helper 和共享 record 放在 `common.rs`;不得把领域聚合或跨层 JSON 兼容结构放入 mapper。 ## API 路由分组 @@ -451,6 +451,30 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - 2026-09-01 修订:每次账号认证、Router Key 准备或 LLM 请求解析既有账号时,api-server 都会在同一 owner 级 provisioning 锁内检查固定套餐 `plan_id=1`。没有 active 订阅、订阅已过期或 `end_time` 距当前 Unix 秒不超过 24 小时时,使用管理员接口新建一条订阅;超过 24 小时则复用现有订阅。订阅检查不进入 Responses 流式 chunk,管理员 Token 缺失时仅保留启动告警并跳过续期检查。 - LLM Router 计费:读取 Router 用户 `used_quota`,以 `50000 quota = 1 泥点` 结算(`500000 quota = 1 USD`,美元数值直接乘 10,不乘汇率)。上游调用前建立首次基线并补结算,成功响应后同步;不足整数部分、扣费失败和余额不足未支付部分留到后续累计同步。扣钱包、写 `llm_router_consume` 流水与推进 checkpoint 在同一事务完成;流水展示“LLM 调用消耗”。历史费用不追扣。详细契约见 `technical/【技术方案】LLM累计额度结算-2026-09-05.md`。 - 2026-09-05 修订:`/api/llm/responses` 与 `/api/llm/chat/completions` 仍在 Router provisioning 前用钱包总余额阻止零余额账号创建或续期;解析凭据后、上游调用前再执行累计额度同步,以扣除退款占用后的剩余可消费余额为准。余额为 `0` 时返回 `409 MUD_POINTS_INSUFFICIENT`;余额或额度同步失败时失败关闭。上游已成功时,后置同步失败只记录错误并留待下次调用前补结算,不把成功模型响应改写为失败。 + +### `payment_app` + +- Rust 结构体:`PaymentApp`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。 +- 作用:平台账号创建的外部支付应用,保存应用归属、展示名称、回调地址、启用状态和更新时间。应用使用平台统一商户主体,不保存支付平台私钥。 +- 索引:`by_payment_app_owner_user_id` 用于个人中心应用列表。 + +### `payment_api_key` + +- Rust 结构体:`PaymentApiKey`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。 +- 作用:支付应用 API Key 元数据。明文只在创建支付应用响应中返回一次,表内仅保存摘要、可见前缀、作用域、使用时间和撤销状态。 +- 索引:`by_payment_api_key_app_id`、`by_payment_api_key_owner_user_id`;`key_hash` 唯一用于支付 API 鉴权。 + +### `payment_order` + +- Rust 结构体:`PaymentOrder`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。 +- 作用:客户端游戏和外部产品的统一收款订单。保存不可变商品 / 金额快照、应用归属、接入方订单号、幂等查找键、微信 Native `code_url`、平台交易号、支付状态、过期时间和到账时间。订单确认只能由验签通知或服务端查单进入 `paid`,不与 `profile_recharge_order` 共享入账路径。 +- 索引:`by_payment_order_app_id`、`by_payment_order_owner_user_id`、`by_payment_order_status`;`idempotency_lookup`、`merchant_order_lookup`、`checkout_token` 唯一用于幂等和公共收银台访问。 + +### `payment_webhook_delivery` + +- Rust 结构体:`PaymentWebhookDelivery`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。 +- 作用:支付成功后的外部回调 durable outbox。保存 callback URL、签名、固定 payload、租约、尝试次数、下次可投递时间、最近错误和 delivered / failed 终态;api-server worker 通过 claim / complete procedure 投递。 +- 索引:`by_payment_webhook_delivery_status_available`、`by_payment_webhook_delivery_owner_user_id`、`by_payment_webhook_delivery_order_id`;`delivery_id` 按订单唯一幂等。 - Windows 私有文件准备:AGC 自有 AppData、凭据目录和 `.agent` 运行态继续使用 managed 范围;用户通过原生选择器明确选中的项目根或文件,若 owner/DACL 仅因权限不足无法读取,则由一次性 UAC helper 在严格复核普通文件/目录、非 reparse/symlink、路径类型和目标 TokenUser 后接管并收紧为当前用户私有 DACL。项目放在当前 profile 之外(例如其他磁盘)不再因为路径位置被拒绝;未经过原生选择器或 AGC 项目根入口的内部路径仍不获得任意提权资格。 ### `game_distribution_game` diff --git a/docs/【技术方案】外部产品支付服务接入-2026-10-03.md b/docs/【技术方案】外部产品支付服务接入-2026-10-03.md new file mode 100644 index 000000000..00143f3bb --- /dev/null +++ b/docs/【技术方案】外部产品支付服务接入-2026-10-03.md @@ -0,0 +1,202 @@ +# 外部产品支付服务接入 + +更新时间:`2026-10-03` + +## 目标 + +为 Genarrative 提供一套由平台统一收款的支付服务,支持: + +- 客户端制作的游戏通过公开接口创建订单并打开 Web 收银台。 +- 外部产品通过应用身份、API Key 和签名调用同一套订单接口。 +- 平台使用已开通的微信支付 / 支付宝商户接口生成动态支付二维码,并以服务端验签、查单或支付通知确认到账。 +- 用户在自己的账户中查看支付服务,创建和配置接入应用,管理 API Key、回调地址和订单。 +- 后台查看支付应用、订单、支付通知、回调投递和异常状态。 +- 首期只负责收款、记账和结果通知;外部产品的余额、手续费、提现和自动分账不在首期范围内,人工结算依据后台账单处理。 + +## 非目标 + +- 不把外部产品订单记入现有的个人钱包充值订单,不向购买者自动发放泥点或会员权益。 +- 不允许客户端或外部产品直接声明“支付成功”改变订单状态。 +- 不保存或向前端下发商户私钥、API v3 密钥、支付宝应用私钥、支付平台 access token 等敏感凭据。 +- 不在首期实现平台余额、外部产品提现、手续费规则、自动分账、佣金和税务结算。 +- 不使用个人收款码、静态收款码或无法验签查单的人工转账作为支付服务正式渠道。 + +## 入口与边界 + +### 参与方 + +- 平台:持有并配置自己的微信支付 / 支付宝商户账户,维护支付服务、订单和通知。 +- 接入方:平台账号下创建的游戏或外部产品应用,使用平台发放的 API Key 创建订单。 +- 购买者:在公共 Web 收银台查看商品信息并完成扫码支付,可不登录平台账号。 +- 支付平台:微信支付 Native、支付宝当面付等已开通的商户 API,负责实际收款、通知和查单。 + +### 入口 + +- 账号侧:`/profile/payment`,展示支付服务状态、应用列表、API Key、回调配置和订单入口。 +- 公共收银台:`/pay/{checkoutToken}`,由服务端返回订单快照、可用支付方式和动态二维码。 +- 外部 API:`/api/external/v1/payment/orders` 及同一资源下的查单、关闭和收银台地址接口。 +- 支付平台通知:按渠道配置独立的 HTTPS 通知入口,统一进入支付订单确认流程。 +- 后台:增加支付服务概览、接入应用、支付订单、通知与回调投递页面,并受现有后台 Tab 权限控制。 + +### 职责边界 + +- `api-server`:HTTP 鉴权、公开 API、收银台 BFF、支付平台通知、回调投递编排和错误 envelope。 +- `module-*`:金额、订单状态、幂等、归属、回调重放和人工复核等领域规则。 +- `spacetime-module`:支付应用、订单、支付尝试、通知观察和投递 outbox 的持久化与原子事务。 +- `spacetime-client`:为 api-server 提供支付领域 procedure 的 typed facade。 +- `platform-*`:微信 / 支付宝的下单、二维码、验签、解密、查单和响应解析。 +- `shared-contracts` / `packages/shared`:账号页、后台页和外部 API 共用的 DTO,不保存业务真相。 +- 前端:展示后端返回的订单状态,收银台轮询或 SSE 只用于刷新,不自行判定到账。 + +## 接入应用与账户配置 + +### 应用 + +每个应用属于一个平台账号,至少包含: + +- `appId`、应用名称、产品类型、启用状态和创建 / 更新时间。 +- 允许使用的支付渠道和金额上下限。 +- 支付成功回调地址、回调签名密钥版本、回调重试策略。 +- 可选的返回地址;必须按 HTTPS、域名白名单和协议校验,不能接受任意用户提交的跳转地址。 +- 应用级订单前缀和展示品牌信息,不能覆盖平台商户主体。 + +### API Key + +- 创建时只展示一次完整 secret,数据库只保存 hash、可见前缀、scope、创建时间、最后使用时间、撤销时间和版本。 +- 首期 scope 至少区分 `payment:orders:create`、`payment:orders:read`、`payment:orders:close`。 +- Key 只属于一个应用,撤销后立即禁止创建订单、查单和关闭操作。 +- 请求使用 `Authorization: Bearer` 或约定的 API Key 头,并要求 `Idempotency-Key`。 +- 管理员页面可以查看安全元数据,不能查看完整 secret。 + +### 商户配置 + +- 商户号、应用 ID、证书序列号、通知地址和支付平台密钥只由服务端环境变量或受控密文配置提供。 +- 用户账户页面只展示已启用的支付渠道和配置状态,不允许用户上传或读取平台级商户私钥。 +- 后台只展示脱敏的商户配置和连通性状态;密钥写入日志、追踪事件和错误响应均禁止。 +- 每个渠道必须明确 `enabled / unavailable / misconfigured` 状态,未完成配置的渠道不出现在收银台可选项。 + +## 订单与金额契约 + +### 创建订单 + +接入方提交: + +- 应用 API Key、幂等键和接入方订单号。 +- 商品标题、商品明细、商品数量、币种和金额,金额使用整数分。 +- 接入方用户标识和可选的业务 metadata;metadata 有字节上限,禁止放入凭据、Data URL 或大段正文。 +- 支付成功后的业务回调要求由应用配置提供,不能在每次请求中绕过已审核的地址白名单。 + +服务端执行: + +1. 校验应用、API Key、scope、幂等键、金额范围、币种和商品字段。 +2. 以 `appId + idempotencyKey` 建立唯一幂等记录;相同请求重放返回原订单,相同幂等键但字段不一致则失败。 +3. 生成平台订单号、不可猜测的 `checkoutToken` 和过期时间,保存不可变商品与金额快照。 +4. 调用选定的支付平台创建订单,要求平台返回的商户订单号、金额和渠道与本地快照一致。 +5. 返回平台无关的订单摘要、收银台 URL、过期时间和可用渠道,不返回商户请求签名或支付平台敏感字段。 + +### 状态 + +订单状态为: + +`pending → paying → paid` + +并允许从 `pending / paying` 进入 `expired / closed`,从 `paid` 进入 `refunded`(退款能力在后续里程碑接入)。支付通知、查单和人工复核只能按允许的状态转移推进;重复通知、乱序通知和重复查单必须幂等。 + +本地 `paid` 必须同时满足: + +- 平台通知验签或主动查单成功。 +- 商户订单号、平台交易号、应用、渠道、金额和币种与本地订单一致。 +- 支付平台确认时间合法且来自平台响应,不能使用服务器当前时间补齐。 + +## Web 收银台 + +收银台使用服务端生成的 `checkoutToken`,页面展示: + +- 商品名称、商品明细、订单号、应付金额和过期时间。 +- 可用支付渠道,例如“微信支付 / 支付宝”,渠道由商户配置和订单约束共同决定。 +- 当前渠道的动态二维码。二维码由平台商户 API 以当前订单金额和平台订单号生成,不复用静态码。 +- “待支付、支付确认中、支付成功、已关闭、已过期、支付异常”等后端状态。 +- 查询状态、复制金额、返回应用等操作;返回地址只使用应用白名单中的地址。 + +收银台不展示平台密钥、用户 API Key、完整回调地址和内部错误文本。浏览器轮询或 SSE 只能读取公开订单状态;出现“支付成功”页面前仍需经过平台回调或服务端查单确认。 + +页面视觉采用截图中的双栏收银台结构作为参考:左侧是订单信息和金额提示,右侧是二维码和渠道状态;移动端改为纵向布局。截图只作为布局参考,不作为支付协议、金额或商户身份依据。 + +## 通知、查单与外部回调 + +- 平台支付通知入口按渠道独立配置,先验签 / 解密,再解析订单和金额,再进入统一的订单确认事务。 +- 通知成功后快速返回平台要求的成功响应;外部产品回调通过 outbox 异步投递,不能让第三方回调慢速阻塞支付平台通知。 +- 外部回调 payload 至少包含平台订单号、接入方订单号、应用 ID、支付状态、金额、币种、平台交易号和确认时间。 +- 首期回调签名使用 `HMAC-SHA256(sha256(rawApiKey), rawBody)`,HTTP 头为 `X-Genarrative-Signature: sha256=`、`X-Genarrative-Event` 和 `X-Genarrative-Delivery-Id`;接入方验签后按订单号和 delivery id 幂等消费。独立可轮换 callback secret 作为后续安全增强项,不在本里程碑伪称已实现。 +- 投递按有限次数退避重试,记录 HTTP 状态、响应摘要、下一次重试时间和最终失败原因;不得记录完整 secret、签名原文或敏感请求体。 +- 查询订单只返回应用归属范围内的订单;公共收银台只返回公开展示字段和状态。 +- 支付确认与唯一回调 outbox 在同一数据库事务提交,回调 payload 从已确认订单构建;失败不会留下 paid 但缺 outbox 的半完成状态。每个投递使用唯一租约代次,过期 worker 不能覆盖新 worker 结果。 +- 外部下单使用不超过 32 字符的平台订单号和冻结的过期时间,独立通知 URL 为 `/api/payment/wechat/notify`;下单 claim 在库内完成,结果不确定时先查单,同一商户单号不因重试更换。 +- 应用、订单、回调 procedure 都要求已注册后端运行服务身份,匿名 SpacetimeDB 调用不得创建或确认订单。回调地址要求 HTTPS、禁止凭据和内网地址,发送时再次检查解析地址且禁止跳转;仅隔离测试环境可以允许 loopback fixture。 +- 支付平台通知、主动查单和后台人工重试全部复用同一条幂等确认路径,不各自实现入账。 + +## 后台数据页 + +后台新增支付服务入口,至少包含: + +- 总览:订单数、已支付金额、待确认订单、通知失败、外部回调失败和按渠道统计。 +- 接入应用:应用状态、所属账号、渠道、回调地址脱敏、Key 状态和最近使用时间。 +- 支付订单:应用、商品、接入方订单号、平台订单号、金额、渠道、状态、创建 / 支付时间和平台交易号。 +- 通知与投递:验签结果、通知类型、订单关联、重复次数、投递状态和下次重试时间。 +- 详情与人工处理:展示本地快照和平台事实的对照;人工操作必须记录管理员、原因、时间和期望状态,不能直接改订单金额或伪造支付成功。 + +分页列表必须使用后端分页和排序参数;金额使用整数分并在展示层格式化,时间统一转成人可读时间,不能把 SATS enum、Option 或微秒 Timestamp 原样展示。 + +## 安全、幂等与失败关闭 + +- 所有金额和商品快照以服务端持久化值为准,客户端传回的金额只作为展示输入,不能作为支付成功依据。 +- 订单创建、支付平台下单、通知确认和外部回调投递分别设置幂等键,重放不会重复创建订单、记账或发放商品。 +- 支付平台超时、网络断开、响应解析失败或结果不确定时,订单保持待确认并进入查单 / 人工复核,不自动标记失败或再次创建新订单。 +- 未知通知、金额不一致、订单归属不一致、签名失败和平台交易号冲突必须拒绝确认,并保留脱敏诊断事实。 +- 日志只记录稳定 HMAC 引用、状态、金额、渠道、阶段和错误分类,不记录二维码原文、密钥、签名、完整订单请求或敏感 metadata。 +- 所有用户、应用、订单、回调和后台查询都检查归属和权限;外部 API 不允许通过猜测订单号访问其它应用数据。 + +## 契约与迁移 + +### API / DTO / OpenAPI + +- 新增支付服务公开 API 时同步更新 `docs/openapi/genarrative-external-v1.openapi.json`、`shared-contracts`、契约测试和公开发现文档。 +- 内部账号接口放在 `/api/profile/payment/*`,后台接口放在 `/admin/api/payment/*`,支付平台通知不走用户鉴权。 +- 公开 DTO 不暴露 provider 原始响应、密钥、完整二维码 payload 或内部数据库 ID。 + +### SpacetimeDB + +计划增加独立的支付领域表,具体表结构在里程碑实施前冻结: + +- 支付接入应用和 API Key 元数据。 +- 平台支付订单和不可变商品快照。 +- 渠道支付尝试与平台交易号。 +- 支付通知观察记录。 +- `payment_webhook_delivery` 外部回调投递 outbox 与投递结果。 + +新增字段必须追加到已有 Rust 表结构体末尾并设置明确默认值;不得重排、改名或删除现有充值表字段。修改 schema 时同步 migration、表目录、生成绑定并运行 `npm run check:spacetime-schema`。 + +### 兼容策略 + +现有个人钱包充值、微信虚拟支付和充值退款链路保持原契约。支付服务用独立订单前缀、表和 API namespace 标识,不把历史充值订单迁移成外部支付订单。 + +## 首期验收标准 + +| 条款 | 验收方式 | 证据 | +| --- | --- | --- | +| 账号可创建支付应用并管理 API Key | 浏览器 smoke、接口测试 | 个人中心页面、Key 只展示一次、撤销立即生效 | +| 客户端游戏和外部产品可创建订单 | API 契约测试 | 同一幂等键返回原订单,字段冲突被拒绝 | +| 收银台展示不可篡改的商品和金额 | 浏览器 smoke | `checkoutToken` 只能读取公开字段,金额来自服务端快照 | +| 商户账户生成动态二维码 | provider mock + 真实沙箱 smoke | 二维码订单号和金额与本地订单一致 | +| 支付结果以服务端事实确认 | 通知 / 查单测试 | 客户端伪造成功不会改变订单,通知重放不重复确认 | +| 外部产品收到成功通知 | outbox 测试与回调 smoke | 签名、重试、最终失败和幂等投递可观测 | +| 后台可查看运营数据 | 后台页面测试 | 应用、订单、通知和回调列表分页、筛选、详情可用 | +| 权限与敏感数据安全 | 负向测试、日志检查 | 跨应用访问、失效 Key、错误金额、错误签名均失败且无密钥泄漏 | +| 首期不产生自动分账 | 领域测试与文档核对 | 没有余额、提现或自动分账写路径 | + +## 未决问题与决策 + +- 首期接入渠道:优先复用现有微信 Native 动态二维码链路;支付宝作为同一收银台的第二个 provider,只有商户 API 配置和验签协议完成后才显示。 +- 首期结算:平台收款、记账、通知,外部产品人工结算;手续费、余额和提现另立规范。 +- 商户主体:使用平台自己的商户号;接入应用不能选择或替换商户主体。 +- 退款:首期只展示支付事实和人工处理入口,正式退款与外部产品权益回收另立里程碑。 diff --git a/packages/shared/src/contracts/payment.ts b/packages/shared/src/contracts/payment.ts new file mode 100644 index 000000000..4683dd077 --- /dev/null +++ b/packages/shared/src/contracts/payment.ts @@ -0,0 +1,55 @@ +export type PaymentApp = { + appId: string; + name: string; + callbackUrl: string | null; + enabled: boolean; + createdAt: string; + updatedAt: string; +}; + +export type PaymentApiKey = { + keyId: string; + appId: string; + name: string; + keyPrefix: string; + scopes: string[]; + createdAt: string; + lastUsedAt: string | null; + revokedAt: string | null; + updatedAt: string; +}; + +export type PaymentAppCreateResponse = { + app: PaymentApp; + apiKey: string; + key: PaymentApiKey; +}; + +export type PaymentAppListResponse = { + apps: PaymentApp[]; + keys: PaymentApiKey[]; +}; + +export type PaymentOrder = { + orderId: string; + appId: string; + merchantOrderId: string; + title: string; + items: unknown[]; + amountCents: number; + currency: string; + provider: string; + providerTradeNo: string | null; + checkoutToken: string; + status: 'pending' | 'paying' | 'paid' | 'expired' | 'closed' | 'refunded'; + checkoutUrl: string; + providerQrCode: string | null; + createdAt: string; + expiresAt: string; + paidAt: string | null; + updatedAt: string; +}; + +export type PaymentOrderCreateResponse = { + order: PaymentOrder; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 87fb5dc00..568bb3382 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,6 +7,7 @@ export * from './contracts/editorScene'; export * from './contracts/externalGeneration'; export * from './contracts/gameDistribution'; export type * from './contracts/hyper3d'; +export * from './contracts/payment'; export * from './contracts/runtime'; export * from './http'; export * from './llm/narrativeLanguage'; diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index eb4b766ac..33140ee88 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -2201,6 +2201,7 @@ fn admin_permission_requirement(_method: &Method, path: &str) -> AdminPermission path if path.starts_with("/admin/api/agc-templates/") => AnyTab(&["agc-templates"]), "/admin/api/dashboard" => AnyTab(&["dashboard"]), "/admin/api/overview" => AnyTab(&["overview"]), + "/admin/api/payment/orders" => AnyTab(&["payment-orders"]), "/admin/api/external-api-keys" => AnyTab(&["tables"]), "/admin/api/debug/http" => AnyTab(&["debug"]), "/admin/api/tracking/events" => AnyTab(&["tracking"]), @@ -2856,6 +2857,15 @@ async fn fetch_admin_dashboard_rows(state: &AppState, sql: &str) -> Result Result, AppError> { + fetch_admin_dashboard_rows(state, sql) + .await + .map_err(|message| AppError::from_status(StatusCode::BAD_GATEWAY).with_message(message)) +} + /// 用户详情累计充值的单次读取上限:单个用户的历史订单远少于该值。 const ADMIN_USER_RECHARGE_SUMMARY_ROW_LIMIT: u32 = 500; diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 659686d1f..390031ef9 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -23,6 +23,7 @@ use crate::{ external_api_auth::ExternalApiPrincipal, http_error::AppError, modules, + payment::handle_payment_wechat_notify, request_context::{RequestContext, attach_request_context, resolve_request_id}, response_headers::propagate_request_id_header, state::{AppState, BackpressureState}, @@ -71,6 +72,10 @@ pub fn build_router(state: AppState) -> Router { get(handle_wechat_virtual_payment_message_push_verify) .post(handle_wechat_virtual_payment_notify), ) + .route( + "/api/payment/wechat/notify", + post(handle_payment_wechat_notify), + ) // HTTP 背压在业务路由外侧快拒绝,避免过载请求继续占用 SpacetimeDB facade 与业务执行资源。 .layer(middleware::from_fn_with_state( BackpressureState::from_ref(&state), diff --git a/server-rs/crates/api-server/src/external_api_auth.rs b/server-rs/crates/api-server/src/external_api_auth.rs index 307203bb1..54f349b53 100644 --- a/server-rs/crates/api-server/src/external_api_auth.rs +++ b/server-rs/crates/api-server/src/external_api_auth.rs @@ -8,7 +8,9 @@ use axum::{ response::Response, }; use serde_json::json; -use spacetime_client::ExternalApiKeyAuthenticateRecordInput; +use spacetime_client::{ + ExternalApiKeyAuthenticateRecordInput, PaymentApiKeyAuthenticateRecordInput, +}; use tracing::warn; use crate::{ @@ -24,6 +26,9 @@ pub struct ExternalApiPrincipal { owner_user_id: String, key_id: String, scopes: Vec, + payment_app_id: Option, + callback_url: Option, + key_hash: String, } impl ExternalApiPrincipal { @@ -33,6 +38,9 @@ impl ExternalApiPrincipal { owner_user_id: owner_user_id.to_string(), key_id: "external-api-key-test".to_string(), scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(), + payment_app_id: None, + callback_url: None, + key_hash: String::new(), } } @@ -44,9 +52,21 @@ impl ExternalApiPrincipal { self.key_id.as_str() } + pub fn payment_app_id(&self) -> Option<&str> { + self.payment_app_id.as_deref() + } + pub fn has_scope(&self, scope: &str) -> bool { self.scopes.iter().any(|item| item == scope) } + + pub fn payment_callback_url(&self) -> Option<&str> { + self.callback_url.as_deref() + } + + pub fn payment_key_hash(&self) -> &str { + self.key_hash.as_str() + } } pub async fn require_external_api_key( @@ -60,10 +80,11 @@ pub async fn require_external_api_key( .map(|context| context.request_id().to_string()) .unwrap_or_else(|| "unknown".to_string()); let raw_key = extract_external_api_bearer(request.headers())?; + let key_hash = hash_external_api_key(raw_key.as_str()); let key = state .authenticator() .authenticate_external_api_key(ExternalApiKeyAuthenticateRecordInput { - key_hash: hash_external_api_key(raw_key.as_str()), + key_hash: key_hash.clone(), used_at_micros: current_utc_micros(), }) .await @@ -79,6 +100,55 @@ pub async fn require_external_api_key( owner_user_id: key.owner_user_id, key_id: key.key_id, scopes: key.scopes, + payment_app_id: None, + callback_url: None, + key_hash: String::new(), + }; + request.extensions_mut().insert(principal.clone()); + + let mut response = next.run(request).await; + response.extensions_mut().insert(principal); + Ok(response) +} + +pub async fn require_payment_api_key( + State(state): State, + mut request: Request, + next: Next, +) -> Result { + let request_id = request + .extensions() + .get::() + .map(|context| context.request_id().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + let raw_key = extract_external_api_bearer(request.headers())?; + let key_hash = hash_external_api_key(raw_key.as_str()); + let (app, key) = state + .authenticator() + .authenticate_payment_api_key(PaymentApiKeyAuthenticateRecordInput { + key_hash: key_hash.clone(), + used_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + warn!( + %request_id, + error = %error, + "支付 API Key 校验失败" + ); + AppError::from_status(StatusCode::UNAUTHORIZED) + .with_message("支付 API Key 不存在或已失效") + })?; + let scopes = serde_json::from_str::>(key.scopes_json.as_str()).map_err(|_| { + AppError::from_status(StatusCode::UNAUTHORIZED).with_message("支付 API Key 权限配置无效") + })?; + let principal = ExternalApiPrincipal { + owner_user_id: app.owner_user_id, + key_id: key.key_id, + scopes, + payment_app_id: Some(app.app_id), + callback_url: app.callback_url, + key_hash, }; request.extensions_mut().insert(principal.clone()); diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 8515721f5..477b7df9c 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -63,6 +63,9 @@ mod modules; mod openai_image_generation; mod password_entry; mod password_management; +mod payment; +mod payment_admin; +mod payment_webhook; mod phone_auth; mod platform_errors; mod process_metrics; @@ -694,6 +697,8 @@ fn spawn_common_app_state_background_workers(state: &AppState) { fn spawn_http_app_state_background_workers(state: &AppState, process_role: ProcessRole) { spawn_common_app_state_background_workers(state); + crate::payment_webhook::PaymentWebhookWorker::new(state.spacetime_client().clone()) + .spawn_worker(); crate::error_reports::spawn_cleanup_worker(state.clone()); if should_start_profile_recharge_expiration_listener(process_role) { spawn_profile_recharge_expiration_listener(state.clone()); diff --git a/server-rs/crates/api-server/src/modules/admin.rs b/server-rs/crates/api-server/src/modules/admin.rs index e16b4a82b..8e40ebb3c 100644 --- a/server-rs/crates/api-server/src/modules/admin.rs +++ b/server-rs/crates/api-server/src/modules/admin.rs @@ -25,6 +25,7 @@ use crate::{ admin_register_recharge_refund, admin_resolve_recharge_refund_manual_review, admin_update_wallet_restriction, }, + payment_admin::admin_list_payment_orders, runtime_profile::{ admin_disable_profile_redeem_code, admin_disable_profile_task_config, admin_get_profile_wallet_config, admin_list_profile_invite_codes, @@ -83,6 +84,7 @@ pub fn router(state: AppState) -> Router { ), ("/admin/api/me", get(admin_me)), ("/admin/api/overview", get(admin_overview)), + ("/admin/api/payment/orders", get(admin_list_payment_orders)), ("/admin/api/dashboard", get(admin_dashboard)), ( "/admin/api/debug/http", diff --git a/server-rs/crates/api-server/src/modules/external_api.rs b/server-rs/crates/api-server/src/modules/external_api.rs index 415a20777..f88171505 100644 --- a/server-rs/crates/api-server/src/modules/external_api.rs +++ b/server-rs/crates/api-server/src/modules/external_api.rs @@ -7,7 +7,9 @@ use axum::{ use crate::{ editor_project::EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES, - external_api_auth::{require_external_api_key, require_external_mcp_api_key}, + external_api_auth::{ + require_external_api_key, require_external_mcp_api_key, require_payment_api_key, + }, external_assets_api::{ confirm_external_asset_object, create_external_direct_upload_ticket, get_external_asset_read_url, @@ -33,6 +35,7 @@ use crate::{ download_external_skill_archive, get_external_agent_integration_manifest, get_external_skill_entry, }, + payment::{create_external_payment_order, get_external_payment_order}, state::AppState, }; @@ -45,7 +48,8 @@ pub fn router(state: AppState) -> Router { require_external_mcp_api_key, )); - let auth = middleware::from_fn_with_state(state, require_external_api_key); + let auth = middleware::from_fn_with_state(state.clone(), require_external_api_key); + let payment_auth = middleware::from_fn_with_state(state.clone(), require_payment_api_key); let protected_routes = [ ( "/api/external/v1/assets/direct-upload-tickets", @@ -157,6 +161,15 @@ pub fn router(state: AppState) -> Router { .fold(Router::new(), |router, (path, methods)| { router.route(path, methods.route_layer(auth.clone())) }); + let payment_router = Router::new() + .route( + "/api/external/v1/payment/orders", + post(create_external_payment_order).route_layer(payment_auth.clone()), + ) + .route( + "/api/external/v1/payment/orders/{order_id}", + get(get_external_payment_order).route_layer(payment_auth), + ); Router::new() .route("/api/external/v1/openapi.json", get(openapi_json)) @@ -173,6 +186,7 @@ pub fn router(state: AppState) -> Router { get(download_external_skill_archive), ) .merge(protected_router) + .merge(payment_router) .merge(mcp_router) } diff --git a/server-rs/crates/api-server/src/modules/profile.rs b/server-rs/crates/api-server/src/modules/profile.rs index de6768102..e558fe7a1 100644 --- a/server-rs/crates/api-server/src/modules/profile.rs +++ b/server-rs/crates/api-server/src/modules/profile.rs @@ -9,6 +9,10 @@ use crate::{ create_external_api_key, ensure_llm_router_api_key, list_external_api_keys, revoke_external_api_key, }, + payment::{ + create_payment_app, get_public_payment_checkout, list_payment_apps, revoke_payment_api_key, + update_payment_app, + }, profile_identity::update_profile_identity, runtime_profile::{ claim_profile_task_reward, confirm_wechat_profile_recharge_order, @@ -59,6 +63,29 @@ pub fn router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/profile/payment/apps", + get(list_payment_apps).post(create_payment_app).route_layer( + middleware::from_fn_with_state(state.clone(), require_bearer_auth), + ), + ) + .route( + "/api/profile/payment/apps/{app_id}", + axum::routing::patch(update_payment_app).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/profile/payment/keys/{key_id}", + axum::routing::delete(revoke_payment_api_key).route_layer( + middleware::from_fn_with_state(state.clone(), require_bearer_auth), + ), + ) + .route( + "/api/payment/checkout/{checkout_token}", + get(get_public_payment_checkout), + ) .route( "/api/profile/wallet-ledger", get(get_profile_wallet_ledger).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/api-server/src/payment.rs b/server-rs/crates/api-server/src/payment.rs new file mode 100644 index 000000000..666edb588 --- /dev/null +++ b/server-rs/crates/api-server/src/payment.rs @@ -0,0 +1,556 @@ +use axum::{ + Extension, Json, + body::Bytes, + extract::{Path, State}, + http::{HeaderMap, StatusCode}, +}; +use hmac::{Hmac, Mac}; +use serde::Deserialize; +use serde_json::{Value, json}; +use sha2::Sha256; +use shared_contracts::payment::{ + PaymentApiKeyResponse, PaymentAppCreateRequest, PaymentAppCreateResponse, + PaymentAppListResponse, PaymentAppResponse, PaymentOrderCreateRequest, + PaymentOrderCreateResponse, PaymentOrderResponse, +}; +use shared_kernel::{build_prefixed_uuid_id, offset_datetime_to_unix_micros, parse_rfc3339}; +use spacetime_client::{ + PaymentApiKeyCreateRecordInput, PaymentAppCreateRecordInput, PaymentOrderCreateRecordInput, + PaymentOrderGetRecordInput, PaymentOrderMarkPaidRecordInput, + PaymentOrderProviderAttemptRecordInput, PaymentOrderProviderCodeRecordInput, + PaymentOrderRecord, +}; + +use crate::{ + api_response::json_success_body, + auth::AuthenticatedAccessToken, + editor_project::current_utc_micros, + external_api_auth::ExternalApiPrincipal, + external_api_keys::hash_external_api_key, + http_error::AppError, + request_context::RequestContext, + state::AppState, + wechat::pay::{build_wechat_payment_service_order_request, map_wechat_pay_error}, +}; + +const PAYMENT_APP_ID_PREFIX: &str = "payment-app-"; +const PAYMENT_KEY_ID_PREFIX: &str = "payment-key-"; +const PAYMENT_CHECKOUT_TOKEN_PREFIX: &str = "checkout-"; +const PAYMENT_RAW_KEY_PREFIX: &str = "tnr_pay_"; +const PAYMENT_CHECKOUT_EXPIRATION_SECONDS: i64 = 5 * 60; +const PAYMENT_IDEMPOTENCY_HEADER: &str = "idempotency-key"; + +pub async fn list_payment_apps( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, AppError> { + let apps = state + .spacetime_client() + .list_payment_apps(authenticated.claims().user_id().to_string()) + .await + .map_err(map_payment_client_error)? + .into_iter() + .map(payment_app_response) + .collect(); + let keys = state + .spacetime_client() + .list_payment_api_keys(authenticated.claims().user_id().to_string()) + .await + .map_err(map_payment_client_error)? + .into_iter() + .map(payment_api_key_response) + .collect(); + + Ok(json_success_body( + Some(&request_context), + PaymentAppListResponse { apps, keys }, + )) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentAppUpdateRequest { + pub enabled: bool, +} + +pub async fn update_payment_app( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Path(app_id): Path, + Json(payload): Json, +) -> Result, AppError> { + let app = state + .spacetime_client() + .update_payment_app(spacetime_client::PaymentAppUpdateRecordInput { + app_id, + owner_user_id: authenticated.claims().user_id().to_string(), + enabled: payload.enabled, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_payment_client_error)?; + Ok(json_success_body( + Some(&request_context), + PaymentAppResponse { + app_id: app.app_id, + name: app.name, + callback_url: app.callback_url, + enabled: app.enabled, + created_at: module_runtime::format_utc_micros(app.created_at_micros), + updated_at: module_runtime::format_utc_micros(app.updated_at_micros), + }, + )) +} + +pub async fn revoke_payment_api_key( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Path(key_id): Path, +) -> Result, AppError> { + let key = state + .spacetime_client() + .revoke_payment_api_key(spacetime_client::PaymentApiKeyRevokeRecordInput { + key_id, + owner_user_id: authenticated.claims().user_id().to_string(), + revoked_at_micros: current_utc_micros(), + }) + .await + .map_err(map_payment_client_error)?; + Ok(json_success_body( + Some(&request_context), + payment_api_key_response(key), + )) +} + +pub async fn create_payment_app( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let user_id = authenticated.claims().user_id().to_string(); + let now_micros = current_utc_micros(); + let app = state + .spacetime_client() + .create_payment_app(PaymentAppCreateRecordInput { + app_id: build_prefixed_uuid_id(PAYMENT_APP_ID_PREFIX), + owner_user_id: user_id.clone(), + name: payload.name, + callback_url: payload.callback_url, + now_micros, + }) + .await + .map_err(map_payment_client_error)?; + + let raw_key = format!( + "{PAYMENT_RAW_KEY_PREFIX}{}.{}", + shared_kernel::new_uuid_simple_string(), + shared_kernel::new_uuid_simple_string() + ); + let key = state + .spacetime_client() + .create_payment_api_key(PaymentApiKeyCreateRecordInput { + key_id: build_prefixed_uuid_id(PAYMENT_KEY_ID_PREFIX), + app_id: app.app_id.clone(), + owner_user_id: user_id, + name: "支付服务默认 Key".to_string(), + key_prefix: raw_key.chars().take(18).collect(), + key_hash: hash_external_api_key(raw_key.as_str()), + scopes: vec![ + module_runtime::PAYMENT_SCOPE_ORDERS_CREATE.to_string(), + module_runtime::PAYMENT_SCOPE_ORDERS_READ.to_string(), + module_runtime::PAYMENT_SCOPE_ORDERS_CLOSE.to_string(), + ], + now_micros, + }) + .await + .map_err(map_payment_client_error)?; + + Ok(json_success_body( + Some(&request_context), + PaymentAppCreateResponse { + app: payment_app_response(app), + api_key: raw_key, + key: payment_api_key_response(key), + }, + )) +} + +pub async fn create_external_payment_order( + State(state): State, + Extension(request_context): Extension, + Extension(principal): Extension, + headers: HeaderMap, + Json(payload): Json, +) -> Result, AppError> { + require_payment_scope(&principal, module_runtime::PAYMENT_SCOPE_ORDERS_CREATE)?; + let app_id = principal.payment_app_id().ok_or_else(|| { + AppError::from_status(StatusCode::FORBIDDEN).with_message("该 Key 不是支付应用 Key") + })?; + let idempotency_key = headers + .get(PAYMENT_IDEMPOTENCY_HEADER) + .and_then(|value| value.to_str().ok()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少 Idempotency-Key") + })? + .to_string(); + if payload.provider != module_runtime::PAYMENT_PROVIDER_WECHAT_NATIVE { + return Err(AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("当前仅支持微信 Native 支付")); + } + let now_micros = current_utc_micros(); + let expires_at_micros = now_micros + .checked_add(PAYMENT_CHECKOUT_EXPIRATION_SECONDS * 1_000_000) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_message("订单有效期无效") + })?; + let items_json = serde_json::to_string(&payload.items).map_err(|_| { + AppError::from_status(StatusCode::BAD_REQUEST).with_message("商品明细格式无效") + })?; + let order = state + .spacetime_client() + .create_payment_order(PaymentOrderCreateRecordInput { + order_id: shared_kernel::new_uuid_simple_string(), + app_id: app_id.to_string(), + owner_user_id: principal.owner_user_id().to_string(), + merchant_order_id: payload.merchant_order_id, + idempotency_lookup: format!("{app_id}:{idempotency_key}"), + title: payload.title, + items_json, + amount_cents: payload.amount_cents, + currency: payload.currency, + provider: module_runtime::PaymentProvider::WechatNative, + checkout_token: format!( + "{PAYMENT_CHECKOUT_TOKEN_PREFIX}{}", + shared_kernel::new_uuid_simple_string() + ), + now_micros, + expires_at_micros, + callback_url: principal.payment_callback_url().map(str::to_string), + callback_signing_key_hash: Some(principal.payment_key_hash().to_string()), + }) + .await + .map_err(map_payment_client_error)?; + + if order.provider_code_url.is_some() + || matches!( + order.status, + module_runtime::PaymentOrderStatus::Paid + | module_runtime::PaymentOrderStatus::Closed + | module_runtime::PaymentOrderStatus::Expired + ) + { + return Ok(json_success_body( + Some(&request_context), + PaymentOrderCreateResponse { + order: payment_order_response(order), + }, + )); + } + + let order = state + .spacetime_client() + .mark_payment_order_provider_attempted(PaymentOrderProviderAttemptRecordInput { + order_id: order.order_id, + attempted_at_micros: current_utc_micros(), + }) + .await + .map_err(map_payment_client_error)?; + let provider_payment = state + .wechat_pay_client() + .create_native_order(build_wechat_payment_service_order_request( + order.order_id.clone(), + order.title.clone(), + order.amount_cents, + resolve_payment_client_ip(&headers), + resolve_payment_notify_url()?, + )) + .await + .map_err(map_wechat_pay_error)?; + let order = state + .spacetime_client() + .set_payment_order_provider_code(PaymentOrderProviderCodeRecordInput { + order_id: order.order_id, + provider_code_url: provider_payment.code_url, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_payment_client_error)?; + + Ok(json_success_body( + Some(&request_context), + PaymentOrderCreateResponse { + order: payment_order_response(order), + }, + )) +} + +pub async fn get_external_payment_order( + State(state): State, + Extension(request_context): Extension, + Extension(principal): Extension, + Path(order_id): Path, +) -> Result, AppError> { + require_payment_scope(&principal, module_runtime::PAYMENT_SCOPE_ORDERS_READ)?; + let order = state + .spacetime_client() + .get_payment_order(PaymentOrderGetRecordInput { + order_id, + owner_user_id: Some(principal.owner_user_id().to_string()), + }) + .await + .map_err(map_payment_client_error)?; + Ok(json_success_body( + Some(&request_context), + PaymentOrderCreateResponse { + order: payment_order_response(order), + }, + )) +} + +pub async fn get_public_payment_checkout( + State(state): State, + Extension(request_context): Extension, + Path(checkout_token): Path, +) -> Result, AppError> { + let order = state + .spacetime_client() + .get_payment_order_by_checkout_token(checkout_token) + .await + .map_err(map_payment_client_error)?; + Ok(json_success_body( + Some(&request_context), + PaymentOrderCreateResponse { + order: payment_order_response(order), + }, + )) +} + +pub async fn handle_payment_wechat_notify( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result { + let notify = state + .wechat_pay_client() + .parse_notify(&headers, &body) + .map_err(map_wechat_pay_error)?; + if notify.trade_state != "SUCCESS" { + return Ok(StatusCode::NO_CONTENT); + } + let transaction_id = notify.transaction_id.ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("微信支付成功通知缺少 transaction_id") + })?; + let amount_cents = notify.amount_total_cents.ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信支付成功通知缺少金额") + })?; + let paid_at_micros = notify + .success_time + .as_deref() + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("微信支付成功通知缺少 success_time") + }) + .and_then(|value| { + parse_rfc3339(value) + .map(offset_datetime_to_unix_micros) + .map_err(|_| { + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("微信支付成功通知 success_time 无效") + }) + })?; + let order = state + .spacetime_client() + .get_payment_order(PaymentOrderGetRecordInput { + order_id: notify.out_trade_no, + owner_user_id: None, + }) + .await + .map_err(map_payment_client_error)?; + if order.provider != module_runtime::PaymentProvider::WechatNative + || order.amount_cents != amount_cents + { + return Err(AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("微信支付通知与本地支付订单不一致")); + } + let paid_order = state + .spacetime_client() + .mark_payment_order_paid(PaymentOrderMarkPaidRecordInput { + order_id: order.order_id, + provider_trade_no: transaction_id, + amount_cents, + paid_at_micros, + }) + .await + .map_err(map_payment_client_error)?; + if paid_order.callback_url.is_some() { + let callback_key = paid_order + .callback_signing_key_hash + .as_deref() + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY) + .with_message("支付订单缺少外部回调签名配置") + })?; + let payload_json = serde_json::to_string(&json!({ + "event": "payment.paid", + "orderId": paid_order.order_id, + "appId": paid_order.app_id, + "merchantOrderId": paid_order.merchant_order_id, + "amountCents": paid_order.amount_cents, + "currency": paid_order.currency, + "provider": paid_order.provider.as_str(), + "providerTradeNo": paid_order.provider_trade_no, + "status": "paid", + "paidAt": paid_order.paid_at_micros.map(module_runtime::format_utc_micros), + })) + .map_err(|_| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_message("支付回调内容序列化失败") + })?; + let signature = payment_webhook_signature(callback_key, payload_json.as_bytes()); + state + .spacetime_client() + .enqueue_payment_webhook(spacetime_client::PaymentWebhookEnqueueRecordInput { + order_id: paid_order.order_id, + payload_json, + signature, + now_micros: current_utc_micros(), + }) + .await + .map_err(map_payment_client_error)?; + } + Ok(StatusCode::NO_CONTENT) +} + +fn payment_webhook_signature(key_hash: &str, payload: &[u8]) -> String { + let mut mac = Hmac::::new_from_slice(key_hash.as_bytes()) + .expect("HMAC accepts a non-empty SHA-256 key hash"); + mac.update(payload); + format!("sha256={}", hex::encode(mac.finalize().into_bytes())) +} + +fn payment_app_response(record: spacetime_client::PaymentAppRecord) -> PaymentAppResponse { + PaymentAppResponse { + app_id: record.app_id, + name: record.name, + callback_url: record.callback_url, + enabled: record.enabled, + created_at: module_runtime::format_utc_micros(record.created_at_micros), + updated_at: module_runtime::format_utc_micros(record.updated_at_micros), + } +} + +fn payment_api_key_response( + record: spacetime_client::PaymentApiKeyRecord, +) -> PaymentApiKeyResponse { + PaymentApiKeyResponse { + key_id: record.key_id, + app_id: record.app_id, + name: record.name, + key_prefix: record.key_prefix, + scopes: serde_json::from_str(&record.scopes_json).unwrap_or_default(), + created_at: module_runtime::format_utc_micros(record.created_at_micros), + last_used_at: record + .last_used_at_micros + .map(module_runtime::format_utc_micros), + revoked_at: record + .revoked_at_micros + .map(module_runtime::format_utc_micros), + updated_at: module_runtime::format_utc_micros(record.updated_at_micros), + } +} + +fn payment_order_response(record: PaymentOrderRecord) -> PaymentOrderResponse { + let items = serde_json::from_str::>(&record.items_json).unwrap_or_default(); + PaymentOrderResponse { + order_id: record.order_id, + app_id: record.app_id, + merchant_order_id: record.merchant_order_id, + title: record.title, + items, + amount_cents: record.amount_cents, + currency: record.currency, + provider: record.provider.as_str().to_string(), + provider_trade_no: record.provider_trade_no, + checkout_token: record.checkout_token.clone(), + status: payment_order_status_text(record.status).to_string(), + checkout_url: format!("/pay/{}", record.checkout_token), + provider_qr_code: record.provider_code_url, + created_at: module_runtime::format_utc_micros(record.created_at_micros), + expires_at: module_runtime::format_utc_micros(record.expires_at_micros), + paid_at: record.paid_at_micros.map(module_runtime::format_utc_micros), + updated_at: module_runtime::format_utc_micros(record.updated_at_micros), + } +} + +fn payment_order_status_text(status: module_runtime::PaymentOrderStatus) -> &'static str { + match status { + module_runtime::PaymentOrderStatus::Pending => "pending", + module_runtime::PaymentOrderStatus::Paying => "paying", + module_runtime::PaymentOrderStatus::Paid => "paid", + module_runtime::PaymentOrderStatus::Expired => "expired", + module_runtime::PaymentOrderStatus::Closed => "closed", + module_runtime::PaymentOrderStatus::Refunded => "refunded", + } +} + +fn require_payment_scope(principal: &ExternalApiPrincipal, scope: &str) -> Result<(), AppError> { + if principal.payment_app_id().is_some() && principal.has_scope(scope) { + return Ok(()); + } + Err(AppError::from_status(StatusCode::FORBIDDEN) + .with_message("支付 API Key 没有当前操作权限") + .with_details(json!({ "scope": scope }))) +} + +fn resolve_payment_client_ip(headers: &HeaderMap) -> String { + headers + .get("x-forwarded-for") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split(',').next()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("127.0.0.1") + .to_string() +} + +fn resolve_payment_notify_url() -> Result { + let value = std::env::var("WECHAT_PAYMENT_NOTIFY_URL") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + if let Some(value) = value { + return Ok(value); + } + if std::env::var("WECHAT_PAY_PROVIDER").ok().as_deref() == Some("mock") { + return Ok("http://127.0.0.1/api/payment/wechat/notify".to_string()); + } + Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) + .with_message("缺少 WECHAT_PAYMENT_NOTIFY_URL,无法创建支付服务订单")) +} + +fn map_payment_client_error(error: spacetime_client::SpacetimeClientError) -> AppError { + match error { + spacetime_client::SpacetimeClientError::Procedure(message) + if message.contains("PAYMENT_PROVIDER_PENDING") => + { + AppError::from_status(StatusCode::CONFLICT) + .with_message("支付平台下单处理中,请稍后查单") + } + spacetime_client::SpacetimeClientError::Procedure(message) + | spacetime_client::SpacetimeClientError::Runtime(message) => { + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("支付服务请求未通过") + .with_details(json!({ "message": message })) + } + other => AppError::from_status(StatusCode::BAD_GATEWAY) + .with_message("支付服务暂时不可用") + .with_details(json!({ "message": other.to_string() })), + } +} diff --git a/server-rs/crates/api-server/src/payment_admin.rs b/server-rs/crates/api-server/src/payment_admin.rs new file mode 100644 index 000000000..3a08bc206 --- /dev/null +++ b/server-rs/crates/api-server/src/payment_admin.rs @@ -0,0 +1,144 @@ +use crate::{ + admin::{AuthenticatedAdmin, fetch_admin_payment_rows}, + http_error::AppError, + request_context::RequestContext, + state::AppState, +}; +use axum::{Extension, Json, extract::State}; +use serde::Serialize; +use serde_json::Value; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdminPaymentOrderEntry { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub title: String, + pub amount_cents: u64, + pub currency: String, + pub provider: String, + pub provider_trade_no: Option, + pub status: String, + pub created_at: String, + pub expires_at: String, + pub paid_at: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdminPaymentOrderListResponse { + pub entries: Vec, + pub webhooks: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdminPaymentWebhookEntry { + pub delivery_id: String, + pub order_id: String, + pub app_id: String, + pub callback_url: String, + pub status: String, + pub attempt_count: u32, + pub available_at: String, + pub last_error_message: Option, + pub updated_at: String, +} + +pub async fn admin_list_payment_orders( + State(state): State, + Extension(_admin): Extension, + Extension(_request_context): Extension, +) -> Result, AppError> { + let rows = fetch_admin_payment_rows( + &state, + "SELECT order_id, app_id, owner_user_id, merchant_order_id, title, amount_cents, currency, provider, provider_trade_no, status, created_at, expires_at, paid_at FROM payment_order ORDER BY created_at DESC LIMIT 200", + ) + .await?; + let entries = rows + .into_iter() + .filter_map(parse_payment_order_row) + .collect(); + let webhook_rows = fetch_admin_payment_rows( + &state, + "SELECT delivery_id, order_id, app_id, callback_url, status, attempt_count, available_at, last_error_message, updated_at FROM payment_webhook_delivery ORDER BY updated_at DESC LIMIT 200", + ) + .await?; + let webhooks = webhook_rows + .into_iter() + .filter_map(parse_payment_webhook_row) + .collect(); + Ok(Json(AdminPaymentOrderListResponse { entries, webhooks })) +} + +fn parse_payment_webhook_row(row: Value) -> Option { + let cells = row.as_array()?; + Some(AdminPaymentWebhookEntry { + delivery_id: cell_text(cells.get(0)?)?, + order_id: cell_text(cells.get(1)?)?, + app_id: cell_text(cells.get(2)?)?, + callback_url: cell_text(cells.get(3)?)?, + status: cell_text(cells.get(4)?)?, + attempt_count: cell_u64(cells.get(5)?)? as u32, + available_at: cell_timestamp(cells.get(6)?), + last_error_message: cell_option_text(cells.get(7)?), + updated_at: cell_timestamp(cells.get(8)?), + }) +} + +fn parse_payment_order_row(row: Value) -> Option { + let cells = row.as_array()?; + Some(AdminPaymentOrderEntry { + order_id: cell_text(cells.get(0)?)?, + app_id: cell_text(cells.get(1)?)?, + owner_user_id: cell_text(cells.get(2)?)?, + merchant_order_id: cell_text(cells.get(3)?)?, + title: cell_text(cells.get(4)?)?, + amount_cents: cell_u64(cells.get(5)?)?, + currency: cell_text(cells.get(6)?)?, + provider: cell_text(cells.get(7)?)?, + provider_trade_no: cell_option_text(cells.get(8)?), + status: cell_text(cells.get(9)?)?, + created_at: cell_timestamp(cells.get(10)?), + expires_at: cell_timestamp(cells.get(11)?), + paid_at: cell_option_timestamp(cells.get(12)?), + }) +} + +fn cell_text(value: &Value) -> Option { + match value { + Value::String(text) => Some(text.clone()), + Value::Number(number) => Some(number.to_string()), + Value::Array(values) if values.len() == 2 => cell_text(values.get(1)?), + Value::Array(values) if values.len() == 1 => cell_text(values.first()?), + Value::Object(object) if object.len() == 1 => cell_text(object.values().next()?), + _ => Some(value.to_string()), + } +} + +fn cell_option_text(value: &Value) -> Option { + if matches!(value, Value::Array(values) if values.first().and_then(Value::as_u64) == Some(1)) { + return None; + } + cell_text(value).filter(|text| !text.is_empty() && text != "null") +} + +fn cell_u64(value: &Value) -> Option { + cell_text(value)?.parse().ok() +} + +fn cell_timestamp(value: &Value) -> String { + cell_text(value) + .and_then(|text| text.parse::().ok()) + .map(module_runtime::format_utc_micros) + .unwrap_or_else(|| "-".to_string()) +} + +fn cell_option_timestamp(value: &Value) -> Option { + if matches!(value, Value::Array(values) if values.first().and_then(Value::as_u64) == Some(1)) { + return None; + } + Some(cell_timestamp(value)) +} diff --git a/server-rs/crates/api-server/src/payment_webhook.rs b/server-rs/crates/api-server/src/payment_webhook.rs new file mode 100644 index 000000000..ae6ba9692 --- /dev/null +++ b/server-rs/crates/api-server/src/payment_webhook.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; +use std::time::Duration; + +use reqwest::header::{CONTENT_TYPE, HeaderName, HeaderValue}; +use spacetime_client::{ + PaymentWebhookClaimRecordInput, PaymentWebhookCompleteRecordInput, + PaymentWebhookDeliveryRecord, SpacetimeClient, +}; +use tokio::time::sleep; +use tracing::{debug, warn}; + +use crate::editor_project::current_utc_micros; + +const PAYMENT_WEBHOOK_WORKER_ID: &str = "api-server-payment-webhook"; +const PAYMENT_WEBHOOK_BATCH_SIZE: u32 = 20; +const PAYMENT_WEBHOOK_INTERVAL: Duration = Duration::from_secs(5); +const PAYMENT_WEBHOOK_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone)] +pub struct PaymentWebhookWorker { + spacetime_client: SpacetimeClient, + http_client: reqwest::Client, +} + +impl PaymentWebhookWorker { + pub fn new(spacetime_client: SpacetimeClient) -> Arc { + Arc::new(Self { + spacetime_client, + http_client: reqwest::Client::builder() + .connect_timeout(Duration::from_secs(3)) + .timeout(PAYMENT_WEBHOOK_REQUEST_TIMEOUT) + .build() + .expect("payment webhook client should build"), + }) + } + + pub fn spawn_worker(self: Arc) { + tokio::spawn(async move { + loop { + if let Err(error) = self.process_once().await { + warn!(error = %error, "支付外部回调 worker 处理失败,将继续重试"); + } + sleep(PAYMENT_WEBHOOK_INTERVAL).await; + } + }); + } + + async fn process_once(&self) -> Result<(), String> { + let now_micros = current_utc_micros(); + let lease_expires_at_micros = now_micros.saturating_add(60_000_000); + let deliveries = self + .spacetime_client + .claim_payment_webhooks(PaymentWebhookClaimRecordInput { + worker_id: PAYMENT_WEBHOOK_WORKER_ID.to_string(), + limit: PAYMENT_WEBHOOK_BATCH_SIZE, + now_micros, + lease_expires_at_micros, + }) + .await + .map_err(|error| error.to_string())?; + + for delivery in deliveries { + self.deliver_one(delivery).await; + } + Ok(()) + } + + async fn deliver_one(&self, delivery: PaymentWebhookDeliveryRecord) { + let result = self + .http_client + .post(&delivery.callback_url) + .header(CONTENT_TYPE, "application/json") + .header( + HeaderName::from_static("x-genarrative-signature"), + HeaderValue::from_str(delivery.signature.as_str()) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), + ) + .header( + HeaderName::from_static("x-genarrative-event"), + "payment.paid", + ) + .header( + HeaderName::from_static("x-genarrative-delivery-id"), + delivery.delivery_id.as_str(), + ) + .body(delivery.payload_json.clone()) + .send() + .await; + + let (success, error_message) = match result { + Ok(response) if response.status().is_success() => (true, None), + Ok(response) => ( + false, + Some(format!("callback HTTP {}", response.status().as_u16())), + ), + Err(error) => (false, Some(format!("callback request failed: {error}"))), + }; + let now_micros = current_utc_micros(); + let next_available_at_micros = if success { + now_micros + } else { + let delay_seconds = 2_i64 + .saturating_pow(delivery.attempt_count.min(7)) + .saturating_mul(5) + .min(300); + now_micros.saturating_add(delay_seconds.saturating_mul(1_000_000)) + }; + if let Err(error) = self + .spacetime_client + .complete_payment_webhook(PaymentWebhookCompleteRecordInput { + delivery_id: delivery.delivery_id.clone(), + worker_id: PAYMENT_WEBHOOK_WORKER_ID.to_string(), + success, + error_message, + now_micros, + next_available_at_micros, + attempt_count: delivery.attempt_count, + }) + .await + { + warn!( + delivery_id = %delivery.delivery_id, + error = %error, + "支付外部回调结果写回失败" + ); + } else { + debug!( + delivery_id = %delivery.delivery_id, + success, + attempt_count = delivery.attempt_count, + "支付外部回调处理完成" + ); + } + } +} diff --git a/server-rs/crates/api-server/src/state/external_api_auth.rs b/server-rs/crates/api-server/src/state/external_api_auth.rs index 2e7f7cb85..93b6d14ac 100644 --- a/server-rs/crates/api-server/src/state/external_api_auth.rs +++ b/server-rs/crates/api-server/src/state/external_api_auth.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use axum::extract::FromRef; use futures_util::future::BoxFuture; use spacetime_client::{ - ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyRecord, SpacetimeClient, + ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyRecord, + PaymentApiKeyAuthenticateRecordInput, PaymentApiKeyRecord, PaymentAppRecord, SpacetimeClient, SpacetimeClientError, }; @@ -14,6 +15,17 @@ pub(crate) trait ExternalApiKeyAuthenticator: Send + Sync { &self, input: ExternalApiKeyAuthenticateRecordInput, ) -> BoxFuture<'_, Result>; + + fn authenticate_payment_api_key( + &self, + _input: PaymentApiKeyAuthenticateRecordInput, + ) -> BoxFuture<'_, Result<(PaymentAppRecord, PaymentApiKeyRecord), SpacetimeClientError>> { + Box::pin(async { + Err(SpacetimeClientError::Procedure( + "支付 API Key 认证未配置".to_string(), + )) + }) + } } impl ExternalApiKeyAuthenticator for SpacetimeClient { @@ -23,6 +35,13 @@ impl ExternalApiKeyAuthenticator for SpacetimeClient { ) -> BoxFuture<'_, Result> { Box::pin(SpacetimeClient::authenticate_external_api_key(self, input)) } + + fn authenticate_payment_api_key( + &self, + input: PaymentApiKeyAuthenticateRecordInput, + ) -> BoxFuture<'_, Result<(PaymentAppRecord, PaymentApiKeyRecord), SpacetimeClientError>> { + Box::pin(SpacetimeClient::authenticate_payment_api_key(self, input)) + } } #[derive(Clone)] diff --git a/server-rs/crates/api-server/src/wechat/pay.rs b/server-rs/crates/api-server/src/wechat/pay.rs index 3cc839884..85ebf9583 100644 --- a/server-rs/crates/api-server/src/wechat/pay.rs +++ b/server-rs/crates/api-server/src/wechat/pay.rs @@ -1089,6 +1089,23 @@ pub fn build_wechat_web_payment_request( description: format!("陶泥儿 - {product_title}"), amount_cents, payer_client_ip, + notify_url: None, + } +} + +pub fn build_wechat_payment_service_order_request( + order_id: String, + product_title: String, + amount_cents: u64, + payer_client_ip: String, + notify_url: String, +) -> WechatWebOrderRequest { + WechatWebOrderRequest { + order_id, + description: format!("陶泥儿 - {product_title}"), + amount_cents, + payer_client_ip, + notify_url: Some(notify_url), } } diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index c6698f7a6..fd5f7252c 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -2,6 +2,8 @@ pub mod agc_analytics; mod agc_models; mod llm_billing; pub use llm_billing::*; +mod payment; +pub use payment::*; mod application; pub use agc_models::*; mod commands; diff --git a/server-rs/crates/module-runtime/src/payment.rs b/server-rs/crates/module-runtime/src/payment.rs new file mode 100644 index 000000000..2a08a934a --- /dev/null +++ b/server-rs/crates/module-runtime/src/payment.rs @@ -0,0 +1,532 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +#[cfg(feature = "spacetime-types")] +use spacetimedb::SpacetimeType; + +pub const PAYMENT_PROVIDER_WECHAT_NATIVE: &str = "wechat_native"; +pub const PAYMENT_SCOPE_ORDERS_CREATE: &str = "payment:orders:create"; +pub const PAYMENT_SCOPE_ORDERS_READ: &str = "payment:orders:read"; +pub const PAYMENT_SCOPE_ORDERS_CLOSE: &str = "payment:orders:close"; +pub const PAYMENT_APP_NAME_MAX_CHARS: usize = 80; +pub const PAYMENT_ORDER_TITLE_MAX_CHARS: usize = 128; +pub const PAYMENT_MERCHANT_ORDER_ID_MAX_CHARS: usize = 96; +pub const PAYMENT_IDEMPOTENCY_KEY_MAX_CHARS: usize = 128; +pub const PAYMENT_CURRENCY_MAX_CHARS: usize = 16; +pub const PAYMENT_ITEMS_MAX_BYTES: usize = 16 * 1024; +pub const PAYMENT_CALLBACK_URL_MAX_CHARS: usize = 512; +pub const PAYMENT_ORDER_MIN_AMOUNT_CENTS: u64 = 1; +pub const PAYMENT_ORDER_MAX_AMOUNT_CENTS: u64 = 9_999_999_999; +pub const PAYMENT_WEBHOOK_MAX_ATTEMPTS: u32 = 8; + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PaymentOrderStatus { + Pending, + Paying, + Paid, + Expired, + Closed, + Refunded, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PaymentProvider { + WechatNative, +} + +impl PaymentProvider { + pub fn as_str(self) -> &'static str { + match self { + Self::WechatNative => PAYMENT_PROVIDER_WECHAT_NATIVE, + } + } +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentAppSnapshot { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub enabled: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentApiKeySnapshot { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub scopes_json: String, + pub created_at_micros: i64, + pub last_used_at_micros: Option, + pub revoked_at_micros: Option, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderSnapshot { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub provider_trade_no: Option, + pub provider_code_url: Option, + pub callback_url: Option, + pub callback_signing_key_hash: Option, + pub provider_attempted_at_micros: Option, + pub checkout_token: String, + pub status: PaymentOrderStatus, + pub created_at_micros: i64, + pub expires_at_micros: i64, + pub paid_at_micros: Option, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentAppCreateInput { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub now_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentApiKeyCreateInput { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub key_hash: String, + pub scopes_json: String, + pub now_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentAppListInput { + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentAppUpdateInput { + pub app_id: String, + pub owner_user_id: String, + pub enabled: bool, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentApiKeyListInput { + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentApiKeyRevokeInput { + pub key_id: String, + pub owner_user_id: String, + pub revoked_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentApiKeyAuthenticateInput { + pub key_hash: String, + pub used_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderCreateInput { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub checkout_token: String, + pub now_micros: i64, + pub expires_at_micros: i64, + pub callback_url: Option, + pub callback_signing_key_hash: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderGetInput { + pub order_id: String, + pub owner_user_id: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderCheckoutGetInput { + pub checkout_token: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderListInput { + pub app_id: Option, + pub owner_user_id: String, + pub limit: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderMarkPaidInput { + pub order_id: String, + pub provider_trade_no: String, + pub amount_cents: u64, + pub paid_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderProviderCodeInput { + pub order_id: String, + pub provider_code_url: String, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderProviderAttemptInput { + pub order_id: String, + pub attempted_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentWebhookDeliverySnapshot { + pub delivery_id: String, + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub callback_url: String, + pub payload_json: String, + pub signature: String, + pub status: String, + pub attempt_count: u32, + pub available_at_micros: i64, + pub lease_worker_id: Option, + pub lease_expires_at_micros: Option, + pub last_error_message: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentWebhookEnqueueInput { + pub order_id: String, + pub payload_json: String, + pub signature: String, + pub now_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentWebhookClaimInput { + pub worker_id: String, + pub limit: u32, + pub now_micros: i64, + pub lease_expires_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentWebhookCompleteInput { + pub delivery_id: String, + pub worker_id: String, + pub success: bool, + pub error_message: Option, + pub now_micros: i64, + pub next_available_at_micros: i64, + pub attempt_count: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentWebhookProcedureResult { + pub ok: bool, + pub delivery: Option, + pub deliveries: Vec, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentAppProcedureResult { + pub ok: bool, + pub app: Option, + pub key: Option, + pub apps: Vec, + pub keys: Vec, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentOrderProcedureResult { + pub ok: bool, + pub order: Option, + pub orders: Vec, + pub error_message: Option, +} + +pub fn build_payment_app_create_input( + app_id: String, + owner_user_id: String, + name: String, + callback_url: Option, + now_micros: i64, +) -> Result { + let app_id = required(app_id, "payment_app.app_id")?; + let owner_user_id = required(owner_user_id, "payment_app.owner_user_id")?; + let name = bounded(name, "payment_app.name", PAYMENT_APP_NAME_MAX_CHARS)?; + let callback_url = callback_url + .map(|value| { + let value = bounded( + value, + "payment_app.callback_url", + PAYMENT_CALLBACK_URL_MAX_CHARS, + )?; + let parsed = Url::parse(value.as_str()) + .map_err(|_| "payment_app.callback_url 格式无效".to_string())?; + let host = parsed + .host_str() + .ok_or_else(|| "payment_app.callback_url 缺少主机名".to_string())?; + let loopback_http = parsed.scheme() == "http" + && (host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .is_ok_and(|address| address.is_loopback())); + if parsed.scheme() != "https" && !loopback_http { + return Err("payment_app.callback_url 必须使用 HTTPS".to_string()); + } + if parsed.username() != "" || parsed.password().is_some() || parsed.fragment().is_some() + { + return Err("payment_app.callback_url 不得包含凭据或 fragment".to_string()); + } + Ok(value) + }) + .transpose()?; + Ok(PaymentAppCreateInput { + app_id, + owner_user_id, + name, + callback_url, + now_micros, + }) +} + +pub fn build_payment_api_key_create_input( + key_id: String, + app_id: String, + owner_user_id: String, + name: String, + key_prefix: String, + key_hash: String, + scopes_json: String, + now_micros: i64, +) -> Result { + Ok(PaymentApiKeyCreateInput { + key_id: required(key_id, "payment_api_key.key_id")?, + app_id: required(app_id, "payment_api_key.app_id")?, + owner_user_id: required(owner_user_id, "payment_api_key.owner_user_id")?, + name: bounded(name, "payment_api_key.name", PAYMENT_APP_NAME_MAX_CHARS)?, + key_prefix: required(key_prefix, "payment_api_key.key_prefix")?, + key_hash: required(key_hash, "payment_api_key.key_hash")?, + scopes_json: bounded(scopes_json, "payment_api_key.scopes_json", 512)?, + now_micros, + }) +} + +pub fn build_payment_order_create_input( + order_id: String, + app_id: String, + owner_user_id: String, + merchant_order_id: String, + idempotency_lookup: String, + title: String, + items_json: String, + amount_cents: u64, + currency: String, + provider: PaymentProvider, + checkout_token: String, + now_micros: i64, + expires_at_micros: i64, + callback_url: Option, + callback_signing_key_hash: Option, +) -> Result { + if !(PAYMENT_ORDER_MIN_AMOUNT_CENTS..=PAYMENT_ORDER_MAX_AMOUNT_CENTS).contains(&amount_cents) { + return Err("payment_order.amount_cents 超出允许范围".to_string()); + } + if currency != "CNY" { + return Err("PAYMENT_INVALID_AMOUNT_OR_CURRENCY".to_string()); + } + let items: serde_json::Value = + serde_json::from_str(&items_json).map_err(|_| "PAYMENT_INVALID_ITEMS".to_string())?; + if !items.is_array() + || items + .as_array() + .is_some_and(|rows| rows.iter().any(|v| !v.is_object())) + || items_json.to_lowercase().contains("data:") + || items_json.to_lowercase().contains("blob:") + { + return Err("PAYMENT_INVALID_ITEMS".to_string()); + } + if items_json.len() > PAYMENT_ITEMS_MAX_BYTES { + return Err("payment_order.items_json 超出大小限制".to_string()); + } + if expires_at_micros <= now_micros { + return Err("payment_order.expires_at_micros 必须晚于创建时间".to_string()); + } + Ok(PaymentOrderCreateInput { + order_id: required(order_id, "payment_order.order_id")?, + app_id: required(app_id, "payment_order.app_id")?, + owner_user_id: required(owner_user_id, "payment_order.owner_user_id")?, + merchant_order_id: bounded( + merchant_order_id, + "payment_order.merchant_order_id", + PAYMENT_MERCHANT_ORDER_ID_MAX_CHARS, + )?, + idempotency_lookup: bounded( + idempotency_lookup, + "payment_order.idempotency_lookup", + PAYMENT_IDEMPOTENCY_KEY_MAX_CHARS + 128, + )?, + title: bounded(title, "payment_order.title", PAYMENT_ORDER_TITLE_MAX_CHARS)?, + items_json, + amount_cents, + currency: bounded( + currency, + "payment_order.currency", + PAYMENT_CURRENCY_MAX_CHARS, + )?, + provider, + checkout_token: required(checkout_token, "payment_order.checkout_token")?, + now_micros, + expires_at_micros, + callback_url: callback_url + .map(|value| { + bounded( + value, + "payment_order.callback_url", + PAYMENT_CALLBACK_URL_MAX_CHARS, + ) + }) + .transpose()?, + callback_signing_key_hash: callback_signing_key_hash + .map(|value| required(value, "payment_order.callback_signing_key_hash")) + .transpose()?, + }) +} + +pub fn build_payment_order_mark_paid_input( + order_id: String, + provider_trade_no: String, + amount_cents: u64, + paid_at_micros: i64, +) -> Result { + if paid_at_micros <= 0 || amount_cents == 0 { + return Err("PAYMENT_INVALID_PROVIDER_FACT".to_string()); + } + Ok(PaymentOrderMarkPaidInput { + order_id: required(order_id, "payment_order.order_id")?, + provider_trade_no: required(provider_trade_no, "payment_order.provider_trade_no")?, + amount_cents, + paid_at_micros, + }) +} + +fn required(value: String, field: &str) -> Result { + let value = value.trim().to_string(); + if value.is_empty() { + return Err(format!("{field} 不能为空")); + } + Ok(value) +} + +fn bounded(value: String, field: &str, max_chars: usize) -> Result { + let value = required(value, field)?; + if value.chars().count() > max_chars { + return Err(format!("{field} 超出长度限制")); + } + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn payment_order_validation_rejects_zero_amount_and_expired_order() { + assert!( + build_payment_order_create_input( + "order".into(), + "app".into(), + "owner".into(), + "merchant".into(), + "lookup".into(), + "商品".into(), + "[]".into(), + 0, + "CNY".into(), + PaymentProvider::WechatNative, + "checkout".into(), + 10, + 20, + None, + None, + ) + .is_err() + ); + assert!( + build_payment_order_create_input( + "order".into(), + "app".into(), + "owner".into(), + "merchant".into(), + "lookup".into(), + "商品".into(), + "[]".into(), + 100, + "CNY".into(), + PaymentProvider::WechatNative, + "checkout".into(), + 20, + 20, + None, + None, + ) + .is_err() + ); + } +} diff --git a/server-rs/crates/platform-wechat/src/pay.rs b/server-rs/crates/platform-wechat/src/pay.rs index d3d862883..075c163ce 100644 --- a/server-rs/crates/platform-wechat/src/pay.rs +++ b/server-rs/crates/platform-wechat/src/pay.rs @@ -192,6 +192,7 @@ pub struct WechatWebOrderRequest { pub description: String, pub amount_cents: u64, pub payer_client_ip: String, + pub notify_url: Option, } #[derive(Clone, Debug)] @@ -985,7 +986,7 @@ impl RealWechatPayClient { description: &request.description, out_trade_no: &request.order_id, time_expire: &expires_at_text, - notify_url: &self.notify_url, + notify_url: request.notify_url.as_deref().unwrap_or(&self.notify_url), amount: WechatJsapiAmount { total: amount_total, currency: "CNY", @@ -1040,7 +1041,7 @@ impl RealWechatPayClient { description: &request.description, out_trade_no: &request.order_id, time_expire: &expires_at_text, - notify_url: &self.notify_url, + notify_url: request.notify_url.as_deref().unwrap_or(&self.notify_url), amount: WechatJsapiAmount { total: amount_total, currency: "CNY", diff --git a/server-rs/crates/shared-contracts/src/lib.rs b/server-rs/crates/shared-contracts/src/lib.rs index 07fbbdf11..ed92100fb 100644 --- a/server-rs/crates/shared-contracts/src/lib.rs +++ b/server-rs/crates/shared-contracts/src/lib.rs @@ -23,4 +23,5 @@ pub mod game_creation_app; pub mod game_distribution; pub mod hyper3d; pub mod llm; +pub mod payment; pub mod runtime; diff --git a/server-rs/crates/shared-contracts/src/payment.rs b/server-rs/crates/shared-contracts/src/payment.rs new file mode 100644 index 000000000..866a4d940 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/payment.rs @@ -0,0 +1,106 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentAppCreateRequest { + pub name: String, + #[serde(default)] + pub callback_url: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentAppResponse { + pub app_id: String, + pub name: String, + pub callback_url: Option, + pub enabled: bool, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentApiKeyResponse { + pub key_id: String, + pub app_id: String, + pub name: String, + pub key_prefix: String, + pub scopes: Vec, + pub created_at: String, + pub last_used_at: Option, + pub revoked_at: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentAppCreateResponse { + pub app: PaymentAppResponse, + pub api_key: String, + pub key: PaymentApiKeyResponse, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentAppListResponse { + pub apps: Vec, + pub keys: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentOrderCreateRequest { + pub merchant_order_id: String, + pub title: String, + #[serde(default)] + pub items: Vec, + pub amount_cents: u64, + #[serde(default = "default_payment_currency")] + pub currency: String, + #[serde(default = "default_payment_provider")] + pub provider: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentOrderResponse { + pub order_id: String, + pub app_id: String, + pub merchant_order_id: String, + pub title: String, + pub items: Vec, + pub amount_cents: u64, + pub currency: String, + pub provider: String, + pub provider_trade_no: Option, + pub checkout_token: String, + pub status: String, + pub checkout_url: String, + pub provider_qr_code: Option, + pub created_at: String, + pub expires_at: String, + pub paid_at: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentOrderCreateResponse { + pub order: PaymentOrderResponse, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentOrderListResponse { + pub orders: Vec, +} + +pub fn default_payment_currency() -> String { + "CNY".to_string() +} + +pub fn default_payment_provider() -> String { + "wechat_native".to_string() +} diff --git a/server-rs/crates/spacetime-client/src/active.rs b/server-rs/crates/spacetime-client/src/active.rs index b0ee057ca..7a1651ae4 100644 --- a/server-rs/crates/spacetime-client/src/active.rs +++ b/server-rs/crates/spacetime-client/src/active.rs @@ -35,6 +35,8 @@ pub use game_distribution::{ #[path = "active/external_generation.rs"] pub mod external_generation; pub mod llm_router_account; +#[path = "active/payment.rs"] +pub mod payment; pub use external_generation::{ ExternalGenerationJobPhaseUpdateError, ExternalGenerationQueueWakeSubscription, }; diff --git a/server-rs/crates/spacetime-client/src/active/mapper.rs b/server-rs/crates/spacetime-client/src/active/mapper.rs index a12c42531..07b1a9cf8 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper.rs @@ -24,6 +24,8 @@ mod external_generation; mod game_distribution; #[path = "mapper/llm_router_account.rs"] pub(crate) mod llm_router_account; +#[path = "mapper/payment.rs"] +mod payment; #[path = "mapper/runtime.rs"] mod runtime; #[path = "mapper/runtime_profile.rs"] @@ -103,6 +105,15 @@ pub use self::game_distribution::{ GameDistributionVersionRecord, }; pub use self::llm_router_account::{LlmRouterAccountRecord, LlmRouterAccountUpsertRecordInput}; +pub use self::payment::{ + PaymentApiKeyAuthenticateRecordInput, PaymentApiKeyCreateRecordInput, PaymentApiKeyRecord, + PaymentApiKeyRevokeRecordInput, PaymentAppCreateRecordInput, PaymentAppRecord, + PaymentAppUpdateRecordInput, PaymentOrderCreateRecordInput, PaymentOrderGetRecordInput, + PaymentOrderListRecordInput, PaymentOrderMarkPaidRecordInput, + PaymentOrderProviderAttemptRecordInput, PaymentOrderProviderCodeRecordInput, + PaymentOrderRecord, PaymentWebhookClaimRecordInput, PaymentWebhookCompleteRecordInput, + PaymentWebhookDeliveryRecord, PaymentWebhookEnqueueRecordInput, +}; pub use self::runtime::FeatureGateConfigRecord; pub(crate) use self::admin_account::{ @@ -161,6 +172,12 @@ pub(crate) use self::game_distribution::{ map_game_distribution_public_game_result, map_game_distribution_review_list_result, map_game_distribution_version_result, map_review_moderation_operation, map_user_review, }; +pub(crate) use self::payment::{ + map_payment_api_key_list_result, map_payment_api_key_result, map_payment_app_list_result, + map_payment_app_result, map_payment_authenticate_result, map_payment_order_list_result, + map_payment_order_result, map_payment_webhook_claim_result, + map_payment_webhook_complete_result, map_payment_webhook_enqueue_result, +}; pub(crate) use self::runtime::{ map_feature_gate_config_procedure_result, map_runtime_setting_procedure_result, map_runtime_tracking_event_batch_procedure_result, map_runtime_tracking_event_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/active/mapper/payment.rs b/server-rs/crates/spacetime-client/src/active/mapper/payment.rs new file mode 100644 index 000000000..43c482d36 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/active/mapper/payment.rs @@ -0,0 +1,516 @@ +use super::*; + +pub type PaymentAppRecord = module_runtime::PaymentAppSnapshot; +pub type PaymentApiKeyRecord = module_runtime::PaymentApiKeySnapshot; +pub type PaymentOrderRecord = module_runtime::PaymentOrderSnapshot; +pub type PaymentWebhookDeliveryRecord = module_runtime::PaymentWebhookDeliverySnapshot; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentAppCreateRecordInput { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentApiKeyCreateRecordInput { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub key_hash: String, + pub scopes: Vec, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentAppUpdateRecordInput { + pub app_id: String, + pub owner_user_id: String, + pub enabled: bool, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentApiKeyRevokeRecordInput { + pub key_id: String, + pub owner_user_id: String, + pub revoked_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentApiKeyAuthenticateRecordInput { + pub key_hash: String, + pub used_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderCreateRecordInput { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: module_runtime::PaymentProvider, + pub checkout_token: String, + pub now_micros: i64, + pub expires_at_micros: i64, + pub callback_url: Option, + pub callback_signing_key_hash: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderGetRecordInput { + pub order_id: String, + pub owner_user_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderListRecordInput { + pub app_id: Option, + pub owner_user_id: String, + pub limit: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderMarkPaidRecordInput { + pub order_id: String, + pub provider_trade_no: String, + pub amount_cents: u64, + pub paid_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderProviderCodeRecordInput { + pub order_id: String, + pub provider_code_url: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentOrderProviderAttemptRecordInput { + pub order_id: String, + pub attempted_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentWebhookEnqueueRecordInput { + pub order_id: String, + pub payload_json: String, + pub signature: String, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentWebhookClaimRecordInput { + pub worker_id: String, + pub limit: u32, + pub now_micros: i64, + pub lease_expires_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentWebhookCompleteRecordInput { + pub delivery_id: String, + pub worker_id: String, + pub success: bool, + pub error_message: Option, + pub now_micros: i64, + pub next_available_at_micros: i64, + pub attempt_count: u32, +} + +impl From for PaymentAppCreateInput { + fn from(input: PaymentAppCreateRecordInput) -> Self { + Self { + app_id: input.app_id, + owner_user_id: input.owner_user_id, + name: input.name, + callback_url: input.callback_url, + now_micros: input.now_micros, + } + } +} + +impl From for PaymentApiKeyCreateInput { + fn from(input: PaymentApiKeyCreateRecordInput) -> Self { + Self { + key_id: input.key_id, + app_id: input.app_id, + owner_user_id: input.owner_user_id, + name: input.name, + key_prefix: input.key_prefix, + key_hash: input.key_hash, + scopes_json: serde_json::to_string(&input.scopes).unwrap_or_else(|_| "[]".to_string()), + now_micros: input.now_micros, + } + } +} + +impl From for PaymentAppUpdateInput { + fn from(input: PaymentAppUpdateRecordInput) -> Self { + Self { + app_id: input.app_id, + owner_user_id: input.owner_user_id, + enabled: input.enabled, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From for PaymentApiKeyRevokeInput { + fn from(input: PaymentApiKeyRevokeRecordInput) -> Self { + Self { + key_id: input.key_id, + owner_user_id: input.owner_user_id, + revoked_at_micros: input.revoked_at_micros, + } + } +} + +impl From for PaymentApiKeyAuthenticateInput { + fn from(input: PaymentApiKeyAuthenticateRecordInput) -> Self { + Self { + key_hash: input.key_hash, + used_at_micros: input.used_at_micros, + } + } +} + +impl From for PaymentOrderCreateInput { + fn from(input: PaymentOrderCreateRecordInput) -> Self { + Self { + order_id: input.order_id, + app_id: input.app_id, + owner_user_id: input.owner_user_id, + merchant_order_id: input.merchant_order_id, + idempotency_lookup: input.idempotency_lookup, + title: input.title, + items_json: input.items_json, + amount_cents: input.amount_cents, + currency: input.currency, + provider: map_payment_provider(input.provider), + checkout_token: input.checkout_token, + now_micros: input.now_micros, + expires_at_micros: input.expires_at_micros, + callback_url: input.callback_url, + callback_signing_key_hash: input.callback_signing_key_hash, + } + } +} + +impl From for PaymentOrderGetInput { + fn from(input: PaymentOrderGetRecordInput) -> Self { + Self { + order_id: input.order_id, + owner_user_id: input.owner_user_id, + } + } +} + +impl From for PaymentOrderListInput { + fn from(input: PaymentOrderListRecordInput) -> Self { + Self { + app_id: input.app_id, + owner_user_id: input.owner_user_id, + limit: input.limit, + } + } +} + +impl From for PaymentOrderMarkPaidInput { + fn from(input: PaymentOrderMarkPaidRecordInput) -> Self { + Self { + order_id: input.order_id, + provider_trade_no: input.provider_trade_no, + amount_cents: input.amount_cents, + paid_at_micros: input.paid_at_micros, + } + } +} + +impl From for PaymentOrderProviderCodeInput { + fn from(input: PaymentOrderProviderCodeRecordInput) -> Self { + Self { + order_id: input.order_id, + provider_code_url: input.provider_code_url, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From for PaymentOrderProviderAttemptInput { + fn from(input: PaymentOrderProviderAttemptRecordInput) -> Self { + Self { + order_id: input.order_id, + attempted_at_micros: input.attempted_at_micros, + } + } +} + +impl From for PaymentWebhookEnqueueInput { + fn from(input: PaymentWebhookEnqueueRecordInput) -> Self { + Self { + order_id: input.order_id, + payload_json: input.payload_json, + signature: input.signature, + now_micros: input.now_micros, + } + } +} + +impl From for PaymentWebhookClaimInput { + fn from(input: PaymentWebhookClaimRecordInput) -> Self { + Self { + worker_id: input.worker_id, + limit: input.limit, + now_micros: input.now_micros, + lease_expires_at_micros: input.lease_expires_at_micros, + } + } +} + +impl From for PaymentWebhookCompleteInput { + fn from(input: PaymentWebhookCompleteRecordInput) -> Self { + Self { + delivery_id: input.delivery_id, + worker_id: input.worker_id, + success: input.success, + error_message: input.error_message, + now_micros: input.now_micros, + next_available_at_micros: input.next_available_at_micros, + attempt_count: input.attempt_count, + } + } +} + +pub(crate) fn map_payment_app_result( + result: PaymentAppProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .app + .map(map_payment_app_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付应用快照")) +} + +pub(crate) fn map_payment_app_list_result( + result: PaymentAppProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(result + .apps + .into_iter() + .map(map_payment_app_snapshot) + .collect()) +} + +pub(crate) fn map_payment_api_key_result( + result: PaymentAppProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .key + .map(map_payment_api_key_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付 API Key 快照")) +} + +pub(crate) fn map_payment_api_key_list_result( + result: PaymentAppProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(result + .keys + .into_iter() + .map(map_payment_api_key_snapshot) + .collect()) +} + +pub(crate) fn map_payment_authenticate_result( + result: PaymentAppProcedureResult, +) -> Result<(PaymentAppRecord, PaymentApiKeyRecord), SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let app = result + .app + .map(map_payment_app_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付应用快照"))?; + let key = result + .key + .map(map_payment_api_key_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付 API Key 快照"))?; + Ok((app, key)) +} + +pub(crate) fn map_payment_order_result( + result: PaymentOrderProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .order + .map(map_payment_order_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付订单快照")) +} + +pub(crate) fn map_payment_order_list_result( + result: PaymentOrderProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(result + .orders + .into_iter() + .map(map_payment_order_snapshot) + .collect()) +} + +fn map_payment_app_snapshot(snapshot: PaymentAppSnapshot) -> PaymentAppRecord { + module_runtime::PaymentAppSnapshot { + app_id: snapshot.app_id, + owner_user_id: snapshot.owner_user_id, + name: snapshot.name, + callback_url: snapshot.callback_url, + enabled: snapshot.enabled, + created_at_micros: snapshot.created_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} + +fn map_payment_api_key_snapshot(snapshot: PaymentApiKeySnapshot) -> PaymentApiKeyRecord { + module_runtime::PaymentApiKeySnapshot { + key_id: snapshot.key_id, + app_id: snapshot.app_id, + owner_user_id: snapshot.owner_user_id, + name: snapshot.name, + key_prefix: snapshot.key_prefix, + scopes_json: snapshot.scopes_json, + created_at_micros: snapshot.created_at_micros, + last_used_at_micros: snapshot.last_used_at_micros, + revoked_at_micros: snapshot.revoked_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} + +fn map_payment_order_snapshot(snapshot: PaymentOrderSnapshot) -> PaymentOrderRecord { + module_runtime::PaymentOrderSnapshot { + order_id: snapshot.order_id, + app_id: snapshot.app_id, + owner_user_id: snapshot.owner_user_id, + merchant_order_id: snapshot.merchant_order_id, + idempotency_lookup: snapshot.idempotency_lookup, + title: snapshot.title, + items_json: snapshot.items_json, + amount_cents: snapshot.amount_cents, + currency: snapshot.currency, + provider: map_payment_provider_back(snapshot.provider), + provider_trade_no: snapshot.provider_trade_no, + provider_code_url: snapshot.provider_code_url, + callback_url: snapshot.callback_url, + callback_signing_key_hash: snapshot.callback_signing_key_hash, + provider_attempted_at_micros: snapshot.provider_attempted_at_micros, + checkout_token: snapshot.checkout_token, + status: map_payment_order_status_back(snapshot.status), + created_at_micros: snapshot.created_at_micros, + expires_at_micros: snapshot.expires_at_micros, + paid_at_micros: snapshot.paid_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} + +fn map_payment_provider(value: module_runtime::PaymentProvider) -> PaymentProvider { + match value { + module_runtime::PaymentProvider::WechatNative => PaymentProvider::WechatNative, + } +} + +fn map_payment_provider_back(value: PaymentProvider) -> module_runtime::PaymentProvider { + match value { + PaymentProvider::WechatNative => module_runtime::PaymentProvider::WechatNative, + } +} + +fn map_payment_order_status_back(value: PaymentOrderStatus) -> module_runtime::PaymentOrderStatus { + match value { + PaymentOrderStatus::Pending => module_runtime::PaymentOrderStatus::Pending, + PaymentOrderStatus::Paying => module_runtime::PaymentOrderStatus::Paying, + PaymentOrderStatus::Paid => module_runtime::PaymentOrderStatus::Paid, + PaymentOrderStatus::Expired => module_runtime::PaymentOrderStatus::Expired, + PaymentOrderStatus::Closed => module_runtime::PaymentOrderStatus::Closed, + PaymentOrderStatus::Refunded => module_runtime::PaymentOrderStatus::Refunded, + } +} + +pub(crate) fn map_payment_webhook_enqueue_result( + result: PaymentWebhookProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(result.delivery.map(map_payment_webhook_snapshot)) +} + +pub(crate) fn map_payment_webhook_claim_result( + result: PaymentWebhookProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + Ok(result + .deliveries + .into_iter() + .map(map_payment_webhook_snapshot) + .collect()) +} + +pub(crate) fn map_payment_webhook_complete_result( + result: PaymentWebhookProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .delivery + .map(map_payment_webhook_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("支付回调投递快照")) +} + +fn map_payment_webhook_snapshot( + snapshot: PaymentWebhookDeliverySnapshot, +) -> PaymentWebhookDeliveryRecord { + module_runtime::PaymentWebhookDeliverySnapshot { + delivery_id: snapshot.delivery_id, + order_id: snapshot.order_id, + app_id: snapshot.app_id, + owner_user_id: snapshot.owner_user_id, + callback_url: snapshot.callback_url, + payload_json: snapshot.payload_json, + signature: snapshot.signature, + status: snapshot.status, + attempt_count: snapshot.attempt_count, + available_at_micros: snapshot.available_at_micros, + lease_worker_id: snapshot.lease_worker_id, + lease_expires_at_micros: snapshot.lease_expires_at_micros, + last_error_message: snapshot.last_error_message, + created_at_micros: snapshot.created_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} diff --git a/server-rs/crates/spacetime-client/src/active/payment.rs b/server-rs/crates/spacetime-client/src/active/payment.rs new file mode 100644 index 000000000..8d1d767e6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/active/payment.rs @@ -0,0 +1,324 @@ +use super::*; + +impl SpacetimeClient { + pub async fn create_payment_app( + &self, + input: PaymentAppCreateRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("create_payment_app", move |connection, sender| { + connection + .procedures() + .create_payment_app_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_app_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn list_payment_apps( + &self, + owner_user_id: String, + ) -> Result, SpacetimeClientError> { + let input = PaymentAppListInput { owner_user_id }; + self.call_after_connect("list_payment_apps", move |connection, sender| { + connection + .procedures() + .list_payment_apps_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_app_list_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn update_payment_app( + &self, + input: PaymentAppUpdateRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("update_payment_app", move |connection, sender| { + connection + .procedures() + .update_payment_app_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_app_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn list_payment_api_keys( + &self, + owner_user_id: String, + ) -> Result, SpacetimeClientError> { + let input = PaymentApiKeyListInput { owner_user_id }; + self.call_after_connect("list_payment_api_keys", move |connection, sender| { + connection + .procedures() + .list_payment_api_keys_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_api_key_list_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn create_payment_api_key( + &self, + input: PaymentApiKeyCreateRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("create_payment_api_key", move |connection, sender| { + connection + .procedures() + .create_payment_api_key_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_api_key_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn revoke_payment_api_key( + &self, + input: PaymentApiKeyRevokeRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("revoke_payment_api_key", move |connection, sender| { + connection + .procedures() + .revoke_payment_api_key_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_api_key_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn authenticate_payment_api_key( + &self, + input: PaymentApiKeyAuthenticateRecordInput, + ) -> Result<(PaymentAppRecord, PaymentApiKeyRecord), SpacetimeClientError> { + let input = input.into(); + self.call_after_connect("authenticate_payment_api_key", move |connection, sender| { + connection + .procedures() + .authenticate_payment_api_key_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_authenticate_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn create_payment_order( + &self, + input: PaymentOrderCreateRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("create_payment_order", move |connection, sender| { + connection + .procedures() + .create_payment_order_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_payment_order( + &self, + input: PaymentOrderGetRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("get_payment_order", move |connection, sender| { + connection + .procedures() + .get_payment_order_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_payment_order_by_checkout_token( + &self, + checkout_token: String, + ) -> Result { + let input = PaymentOrderCheckoutGetInput { checkout_token }; + self.call_after_connect( + "get_payment_order_by_checkout_token", + move |connection, sender| { + connection + .procedures() + .get_payment_order_by_checkout_token_and_return_then( + input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn list_payment_orders( + &self, + input: PaymentOrderListRecordInput, + ) -> Result, SpacetimeClientError> { + let input = input.into(); + self.call_after_connect("list_payment_orders", move |connection, sender| { + connection + .procedures() + .list_payment_orders_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_list_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn mark_payment_order_paid( + &self, + input: PaymentOrderMarkPaidRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("mark_payment_order_paid", move |connection, sender| { + connection + .procedures() + .mark_payment_order_paid_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn set_payment_order_provider_code( + &self, + input: PaymentOrderProviderCodeRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect( + "set_payment_order_provider_code", + move |connection, sender| { + connection + .procedures() + .set_payment_order_provider_code_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }); + }, + ) + .await + } + + pub async fn mark_payment_order_provider_attempted( + &self, + input: PaymentOrderProviderAttemptRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect( + "mark_payment_order_provider_attempted", + move |connection, sender| { + connection + .procedures() + .mark_payment_order_provider_attempted_and_return_then( + input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_order_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn enqueue_payment_webhook( + &self, + input: PaymentWebhookEnqueueRecordInput, + ) -> Result, SpacetimeClientError> { + let input = input.into(); + self.call_after_connect("enqueue_payment_webhook", move |connection, sender| { + connection + .procedures() + .enqueue_payment_webhook_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_webhook_enqueue_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn claim_payment_webhooks( + &self, + input: PaymentWebhookClaimRecordInput, + ) -> Result, SpacetimeClientError> { + let input = input.into(); + self.call_after_connect("claim_payment_webhooks", move |connection, sender| { + connection + .procedures() + .claim_payment_webhooks_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_webhook_claim_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn complete_payment_webhook( + &self, + input: PaymentWebhookCompleteRecordInput, + ) -> Result { + let input = input.into(); + self.call_after_connect("complete_payment_webhook", move |connection, sender| { + connection + .procedures() + .complete_payment_webhook_and_return_then(input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_payment_webhook_complete_result); + send_once(&sender, mapped); + }); + }) + .await + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index 0946050b7..805d286be 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -134,6 +134,7 @@ pub mod auth_store_projection_user_type; pub mod auth_store_projection_view_type; pub mod auth_store_projection_wechat_state_type; pub mod authenticate_external_api_key_and_return_procedure; +pub mod authenticate_payment_api_key_and_return_procedure; pub mod authorize_database_migration_operator_procedure; pub mod backfill_editor_canvas_layout_and_return_procedure; pub mod backfill_external_generation_job_summaries_and_return_procedure; @@ -142,6 +143,7 @@ pub mod bind_asset_object_to_entity_reducer; pub mod cancel_ai_task_and_return_procedure; pub mod cancel_game_distribution_version_and_return_procedure; pub mod claim_external_generation_jobs_and_return_procedure; +pub mod claim_payment_webhooks_and_return_procedure; pub mod claim_profile_recharge_order_expiration_schedule_and_return_procedure; pub mod claim_profile_task_reward_and_return_procedure; pub mod clean_editor_image_asset_kind_and_return_procedure; @@ -152,6 +154,7 @@ pub mod complete_ai_stage_and_return_procedure; pub mod complete_ai_task_and_return_procedure; pub mod complete_editor_asset_group_cohort_and_return_procedure; pub mod complete_external_generation_job_and_return_procedure; +pub mod complete_payment_webhook_and_return_procedure; pub mod complete_profile_recharge_order_expiration_schedule_and_return_procedure; pub mod confirm_asset_object_and_return_procedure; pub mod confirm_asset_object_reducer; @@ -169,6 +172,9 @@ pub mod create_error_report_and_return_procedure; pub mod create_external_api_key_and_return_procedure; pub mod create_game_distribution_game_and_return_procedure; pub mod create_game_distribution_version_and_return_procedure; +pub mod create_payment_api_key_and_return_procedure; +pub mod create_payment_app_and_return_procedure; +pub mod create_payment_order_and_return_procedure; pub mod create_profile_recharge_order_and_return_procedure; pub mod creation_entry_config_table; pub mod creation_entry_config_type; @@ -337,6 +343,7 @@ pub mod editor_spritesheet_slice_batch_persist_result_type; pub mod editor_spritesheet_slice_persist_item_input_type; pub mod editor_spritesheet_slice_persisted_item_type; pub mod enqueue_external_generation_job_and_return_procedure; +pub mod enqueue_payment_webhook_and_return_procedure; pub mod enqueue_profile_wallet_refund_outbox_and_return_procedure; pub mod ensure_analytics_date_dimension_for_date_reducer; pub mod error_report_create_input_type; @@ -477,6 +484,8 @@ pub mod get_game_distribution_my_review_and_return_procedure; pub mod get_game_distribution_version_and_return_procedure; pub mod get_llm_router_account_and_return_procedure; pub mod get_owner_game_distribution_version_and_return_procedure; +pub mod get_payment_order_and_return_procedure; +pub mod get_payment_order_by_checkout_token_and_return_procedure; pub mod get_profile_dashboard_procedure; pub mod get_profile_recharge_center_procedure; pub mod get_profile_recharge_order_and_return_procedure; @@ -509,6 +518,9 @@ pub mod list_game_distribution_review_games_and_return_procedure; pub mod list_game_distribution_reviews_and_return_procedure; pub mod list_game_distribution_user_reviews_and_return_procedure; pub mod list_owner_game_distribution_games_and_return_procedure; +pub mod list_payment_api_keys_and_return_procedure; +pub mod list_payment_apps_and_return_procedure; +pub mod list_payment_orders_and_return_procedure; pub mod list_profile_recharge_refund_holds_for_reconciliation_procedure; pub mod list_profile_recharge_refunds_for_reconciliation_procedure; pub mod list_profile_wallet_ledger_procedure; @@ -528,10 +540,46 @@ pub mod llm_router_billing_checkpoint_type; pub mod llm_router_quota_settlement_input_type; pub mod llm_router_quota_settlement_result_type; pub mod mark_editor_showcase_asset_refunded_and_return_procedure; +pub mod mark_payment_order_paid_and_return_procedure; +pub mod mark_payment_order_provider_attempted_and_return_procedure; pub mod mark_profile_recharge_order_expiration_checked_procedure; pub mod mark_profile_recharge_order_paid_and_return_procedure; pub mod moderate_game_distribution_user_review_and_return_procedure; pub mod normalize_editor_character_animation_metadata_and_return_procedure; +pub mod payment_api_key_authenticate_input_type; +pub mod payment_api_key_create_input_type; +pub mod payment_api_key_list_input_type; +pub mod payment_api_key_revoke_input_type; +pub mod payment_api_key_snapshot_type; +pub mod payment_api_key_table; +pub mod payment_api_key_type; +pub mod payment_app_create_input_type; +pub mod payment_app_list_input_type; +pub mod payment_app_procedure_result_type; +pub mod payment_app_snapshot_type; +pub mod payment_app_table; +pub mod payment_app_type; +pub mod payment_app_update_input_type; +pub mod payment_order_checkout_get_input_type; +pub mod payment_order_create_input_type; +pub mod payment_order_get_input_type; +pub mod payment_order_list_input_type; +pub mod payment_order_mark_paid_input_type; +pub mod payment_order_procedure_result_type; +pub mod payment_order_provider_attempt_input_type; +pub mod payment_order_provider_code_input_type; +pub mod payment_order_snapshot_type; +pub mod payment_order_status_type; +pub mod payment_order_table; +pub mod payment_order_type; +pub mod payment_provider_type; +pub mod payment_webhook_claim_input_type; +pub mod payment_webhook_complete_input_type; +pub mod payment_webhook_delivery_snapshot_type; +pub mod payment_webhook_delivery_table; +pub mod payment_webhook_delivery_type; +pub mod payment_webhook_enqueue_input_type; +pub mod payment_webhook_procedure_result_type; pub mod persist_editor_generation_result_and_return_procedure; pub mod persist_editor_pixel_art_result_and_return_procedure; pub mod persist_editor_spritesheet_slice_batch_and_return_procedure; @@ -625,6 +673,7 @@ pub mod resolve_profile_recharge_refund_manual_review_and_return_procedure; pub mod restore_game_distribution_game_and_return_procedure; pub mod revoke_database_migration_operator_procedure; pub mod revoke_external_api_key_and_return_procedure; +pub mod revoke_payment_api_key_and_return_procedure; pub mod rollback_editor_canvas_layout_and_return_procedure; pub mod rotate_editor_generation_runtime_service_identity_and_return_procedure; pub mod runtime_browse_history_theme_mode_type; @@ -776,6 +825,7 @@ pub mod save_editor_project_layout_v_2_and_return_procedure; pub mod save_game_distribution_my_review_and_return_procedure; pub mod seed_analytics_date_dimensions_reducer; pub mod set_editor_showcase_asset_like_for_viewer_and_return_procedure; +pub mod set_payment_order_provider_code_and_return_procedure; pub mod settle_llm_router_quota_and_return_procedure; pub mod start_ai_task_reducer; pub mod start_ai_task_stage_reducer; @@ -797,6 +847,7 @@ pub mod update_editor_project_resource_showcase_and_return_procedure; pub mod update_editor_showcase_asset_display_and_return_procedure; pub mod update_error_report_and_return_procedure; pub mod update_external_generation_job_phase_and_return_procedure; +pub mod update_payment_app_and_return_procedure; pub mod upload_agc_analytics_batch_procedure; pub mod upsert_editor_generation_pricing_config_and_return_procedure; pub mod upsert_editor_showcase_campaign_config_and_return_procedure; @@ -937,6 +988,7 @@ pub use auth_store_projection_user_type::AuthStoreProjectionUser; pub use auth_store_projection_view_type::AuthStoreProjectionView; pub use auth_store_projection_wechat_state_type::AuthStoreProjectionWechatState; pub use authenticate_external_api_key_and_return_procedure::authenticate_external_api_key_and_return; +pub use authenticate_payment_api_key_and_return_procedure::authenticate_payment_api_key_and_return; pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator; pub use backfill_editor_canvas_layout_and_return_procedure::backfill_editor_canvas_layout_and_return; pub use backfill_external_generation_job_summaries_and_return_procedure::backfill_external_generation_job_summaries_and_return; @@ -945,6 +997,7 @@ pub use bind_asset_object_to_entity_reducer::bind_asset_object_to_entity; pub use cancel_ai_task_and_return_procedure::cancel_ai_task_and_return; pub use cancel_game_distribution_version_and_return_procedure::cancel_game_distribution_version_and_return; pub use claim_external_generation_jobs_and_return_procedure::claim_external_generation_jobs_and_return; +pub use claim_payment_webhooks_and_return_procedure::claim_payment_webhooks_and_return; pub use claim_profile_recharge_order_expiration_schedule_and_return_procedure::claim_profile_recharge_order_expiration_schedule_and_return; pub use claim_profile_task_reward_and_return_procedure::claim_profile_task_reward_and_return; pub use clean_editor_image_asset_kind_and_return_procedure::clean_editor_image_asset_kind_and_return; @@ -955,6 +1008,7 @@ pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return; pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return; pub use complete_editor_asset_group_cohort_and_return_procedure::complete_editor_asset_group_cohort_and_return; pub use complete_external_generation_job_and_return_procedure::complete_external_generation_job_and_return; +pub use complete_payment_webhook_and_return_procedure::complete_payment_webhook_and_return; pub use complete_profile_recharge_order_expiration_schedule_and_return_procedure::complete_profile_recharge_order_expiration_schedule_and_return; pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return; pub use confirm_asset_object_reducer::confirm_asset_object; @@ -972,6 +1026,9 @@ pub use create_error_report_and_return_procedure::create_error_report_and_return pub use create_external_api_key_and_return_procedure::create_external_api_key_and_return; pub use create_game_distribution_game_and_return_procedure::create_game_distribution_game_and_return; pub use create_game_distribution_version_and_return_procedure::create_game_distribution_version_and_return; +pub use create_payment_api_key_and_return_procedure::create_payment_api_key_and_return; +pub use create_payment_app_and_return_procedure::create_payment_app_and_return; +pub use create_payment_order_and_return_procedure::create_payment_order_and_return; pub use create_profile_recharge_order_and_return_procedure::create_profile_recharge_order_and_return; pub use creation_entry_config_table::*; pub use creation_entry_config_type::CreationEntryConfig; @@ -1140,6 +1197,7 @@ pub use editor_spritesheet_slice_batch_persist_result_type::EditorSpritesheetSli pub use editor_spritesheet_slice_persist_item_input_type::EditorSpritesheetSlicePersistItemInput; pub use editor_spritesheet_slice_persisted_item_type::EditorSpritesheetSlicePersistedItem; pub use enqueue_external_generation_job_and_return_procedure::enqueue_external_generation_job_and_return; +pub use enqueue_payment_webhook_and_return_procedure::enqueue_payment_webhook_and_return; pub use enqueue_profile_wallet_refund_outbox_and_return_procedure::enqueue_profile_wallet_refund_outbox_and_return; pub use ensure_analytics_date_dimension_for_date_reducer::ensure_analytics_date_dimension_for_date; pub use error_report_create_input_type::ErrorReportCreateInput; @@ -1280,6 +1338,8 @@ pub use get_game_distribution_my_review_and_return_procedure::get_game_distribut pub use get_game_distribution_version_and_return_procedure::get_game_distribution_version_and_return; pub use get_llm_router_account_and_return_procedure::get_llm_router_account_and_return; pub use get_owner_game_distribution_version_and_return_procedure::get_owner_game_distribution_version_and_return; +pub use get_payment_order_and_return_procedure::get_payment_order_and_return; +pub use get_payment_order_by_checkout_token_and_return_procedure::get_payment_order_by_checkout_token_and_return; pub use get_profile_dashboard_procedure::get_profile_dashboard; pub use get_profile_recharge_center_procedure::get_profile_recharge_center; pub use get_profile_recharge_order_and_return_procedure::get_profile_recharge_order_and_return; @@ -1312,6 +1372,9 @@ pub use list_game_distribution_review_games_and_return_procedure::list_game_dist pub use list_game_distribution_reviews_and_return_procedure::list_game_distribution_reviews_and_return; pub use list_game_distribution_user_reviews_and_return_procedure::list_game_distribution_user_reviews_and_return; pub use list_owner_game_distribution_games_and_return_procedure::list_owner_game_distribution_games_and_return; +pub use list_payment_api_keys_and_return_procedure::list_payment_api_keys_and_return; +pub use list_payment_apps_and_return_procedure::list_payment_apps_and_return; +pub use list_payment_orders_and_return_procedure::list_payment_orders_and_return; pub use list_profile_recharge_refund_holds_for_reconciliation_procedure::list_profile_recharge_refund_holds_for_reconciliation; pub use list_profile_recharge_refunds_for_reconciliation_procedure::list_profile_recharge_refunds_for_reconciliation; pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger; @@ -1331,10 +1394,46 @@ pub use llm_router_billing_checkpoint_type::LlmRouterBillingCheckpoint; pub use llm_router_quota_settlement_input_type::LlmRouterQuotaSettlementInput; pub use llm_router_quota_settlement_result_type::LlmRouterQuotaSettlementResult; pub use mark_editor_showcase_asset_refunded_and_return_procedure::mark_editor_showcase_asset_refunded_and_return; +pub use mark_payment_order_paid_and_return_procedure::mark_payment_order_paid_and_return; +pub use mark_payment_order_provider_attempted_and_return_procedure::mark_payment_order_provider_attempted_and_return; pub use mark_profile_recharge_order_expiration_checked_procedure::mark_profile_recharge_order_expiration_checked; pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return; pub use moderate_game_distribution_user_review_and_return_procedure::moderate_game_distribution_user_review_and_return; pub use normalize_editor_character_animation_metadata_and_return_procedure::normalize_editor_character_animation_metadata_and_return; +pub use payment_api_key_authenticate_input_type::PaymentApiKeyAuthenticateInput; +pub use payment_api_key_create_input_type::PaymentApiKeyCreateInput; +pub use payment_api_key_list_input_type::PaymentApiKeyListInput; +pub use payment_api_key_revoke_input_type::PaymentApiKeyRevokeInput; +pub use payment_api_key_snapshot_type::PaymentApiKeySnapshot; +pub use payment_api_key_table::*; +pub use payment_api_key_type::PaymentApiKey; +pub use payment_app_create_input_type::PaymentAppCreateInput; +pub use payment_app_list_input_type::PaymentAppListInput; +pub use payment_app_procedure_result_type::PaymentAppProcedureResult; +pub use payment_app_snapshot_type::PaymentAppSnapshot; +pub use payment_app_table::*; +pub use payment_app_type::PaymentApp; +pub use payment_app_update_input_type::PaymentAppUpdateInput; +pub use payment_order_checkout_get_input_type::PaymentOrderCheckoutGetInput; +pub use payment_order_create_input_type::PaymentOrderCreateInput; +pub use payment_order_get_input_type::PaymentOrderGetInput; +pub use payment_order_list_input_type::PaymentOrderListInput; +pub use payment_order_mark_paid_input_type::PaymentOrderMarkPaidInput; +pub use payment_order_procedure_result_type::PaymentOrderProcedureResult; +pub use payment_order_provider_attempt_input_type::PaymentOrderProviderAttemptInput; +pub use payment_order_provider_code_input_type::PaymentOrderProviderCodeInput; +pub use payment_order_snapshot_type::PaymentOrderSnapshot; +pub use payment_order_status_type::PaymentOrderStatus; +pub use payment_order_table::*; +pub use payment_order_type::PaymentOrder; +pub use payment_provider_type::PaymentProvider; +pub use payment_webhook_claim_input_type::PaymentWebhookClaimInput; +pub use payment_webhook_complete_input_type::PaymentWebhookCompleteInput; +pub use payment_webhook_delivery_snapshot_type::PaymentWebhookDeliverySnapshot; +pub use payment_webhook_delivery_table::*; +pub use payment_webhook_delivery_type::PaymentWebhookDelivery; +pub use payment_webhook_enqueue_input_type::PaymentWebhookEnqueueInput; +pub use payment_webhook_procedure_result_type::PaymentWebhookProcedureResult; pub use persist_editor_generation_result_and_return_procedure::persist_editor_generation_result_and_return; pub use persist_editor_pixel_art_result_and_return_procedure::persist_editor_pixel_art_result_and_return; pub use persist_editor_spritesheet_slice_batch_and_return_procedure::persist_editor_spritesheet_slice_batch_and_return; @@ -1428,6 +1527,7 @@ pub use resolve_profile_recharge_refund_manual_review_and_return_procedure::reso pub use restore_game_distribution_game_and_return_procedure::restore_game_distribution_game_and_return; pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator; pub use revoke_external_api_key_and_return_procedure::revoke_external_api_key_and_return; +pub use revoke_payment_api_key_and_return_procedure::revoke_payment_api_key_and_return; pub use rollback_editor_canvas_layout_and_return_procedure::rollback_editor_canvas_layout_and_return; pub use rotate_editor_generation_runtime_service_identity_and_return_procedure::rotate_editor_generation_runtime_service_identity_and_return; pub use runtime_browse_history_theme_mode_type::RuntimeBrowseHistoryThemeMode; @@ -1579,6 +1679,7 @@ pub use save_editor_project_layout_v_2_and_return_procedure::save_editor_project pub use save_game_distribution_my_review_and_return_procedure::save_game_distribution_my_review_and_return; pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions; pub use set_editor_showcase_asset_like_for_viewer_and_return_procedure::set_editor_showcase_asset_like_for_viewer_and_return; +pub use set_payment_order_provider_code_and_return_procedure::set_payment_order_provider_code_and_return; pub use settle_llm_router_quota_and_return_procedure::settle_llm_router_quota_and_return; pub use start_ai_task_reducer::start_ai_task; pub use start_ai_task_stage_reducer::start_ai_task_stage; @@ -1600,6 +1701,7 @@ pub use update_editor_project_resource_showcase_and_return_procedure::update_edi pub use update_editor_showcase_asset_display_and_return_procedure::update_editor_showcase_asset_display_and_return; pub use update_error_report_and_return_procedure::update_error_report_and_return; pub use update_external_generation_job_phase_and_return_procedure::update_external_generation_job_phase_and_return; +pub use update_payment_app_and_return_procedure::update_payment_app_and_return; pub use upload_agc_analytics_batch_procedure::upload_agc_analytics_batch; pub use upsert_editor_generation_pricing_config_and_return_procedure::upsert_editor_generation_pricing_config_and_return; pub use upsert_editor_showcase_campaign_config_and_return_procedure::upsert_editor_showcase_campaign_config_and_return; @@ -1772,6 +1874,10 @@ pub struct DbUpdate { game_distribution_version: __sdk::TableUpdate, llm_router_account: __sdk::TableUpdate, llm_router_billing_checkpoint: __sdk::TableUpdate, + payment_api_key: __sdk::TableUpdate, + payment_app: __sdk::TableUpdate, + payment_order: __sdk::TableUpdate, + payment_webhook_delivery: __sdk::TableUpdate, profile_code_operation: __sdk::TableUpdate, profile_daily_free_points: __sdk::TableUpdate, profile_dashboard_state: __sdk::TableUpdate, @@ -2003,6 +2109,18 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "llm_router_billing_checkpoint" => db_update.llm_router_billing_checkpoint.append( llm_router_billing_checkpoint_table::parse_table_update(table_update)?, ), + "payment_api_key" => db_update + .payment_api_key + .append(payment_api_key_table::parse_table_update(table_update)?), + "payment_app" => db_update + .payment_app + .append(payment_app_table::parse_table_update(table_update)?), + "payment_order" => db_update + .payment_order + .append(payment_order_table::parse_table_update(table_update)?), + "payment_webhook_delivery" => db_update.payment_webhook_delivery.append( + payment_webhook_delivery_table::parse_table_update(table_update)?, + ), "profile_code_operation" => db_update.profile_code_operation.append( profile_code_operation_table::parse_table_update(table_update)?, ), @@ -2414,6 +2532,21 @@ impl __sdk::DbUpdate for DbUpdate { &self.llm_router_billing_checkpoint, ) .with_updates_by_pk(|row| &row.account_key); + diff.payment_api_key = cache + .apply_diff_to_table::("payment_api_key", &self.payment_api_key) + .with_updates_by_pk(|row| &row.key_id); + diff.payment_app = cache + .apply_diff_to_table::("payment_app", &self.payment_app) + .with_updates_by_pk(|row| &row.app_id); + diff.payment_order = cache + .apply_diff_to_table::("payment_order", &self.payment_order) + .with_updates_by_pk(|row| &row.order_id); + diff.payment_webhook_delivery = cache + .apply_diff_to_table::( + "payment_webhook_delivery", + &self.payment_webhook_delivery, + ) + .with_updates_by_pk(|row| &row.delivery_id); diff.profile_code_operation = cache .apply_diff_to_table::( "profile_code_operation", @@ -2775,6 +2908,18 @@ impl __sdk::DbUpdate for DbUpdate { "llm_router_billing_checkpoint" => db_update .llm_router_billing_checkpoint .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "payment_api_key" => db_update + .payment_api_key + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "payment_app" => db_update + .payment_app + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "payment_order" => db_update + .payment_order + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "payment_webhook_delivery" => db_update + .payment_webhook_delivery + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_code_operation" => db_update .profile_code_operation .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3049,6 +3194,18 @@ impl __sdk::DbUpdate for DbUpdate { "llm_router_billing_checkpoint" => db_update .llm_router_billing_checkpoint .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "payment_api_key" => db_update + .payment_api_key + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "payment_app" => db_update + .payment_app + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "payment_order" => db_update + .payment_order + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "payment_webhook_delivery" => db_update + .payment_webhook_delivery + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_code_operation" => db_update .profile_code_operation .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3229,6 +3386,10 @@ pub struct AppliedDiff<'r> { game_distribution_version: __sdk::TableAppliedDiff<'r, GameDistributionVersion>, llm_router_account: __sdk::TableAppliedDiff<'r, LlmRouterAccount>, llm_router_billing_checkpoint: __sdk::TableAppliedDiff<'r, LlmRouterBillingCheckpoint>, + payment_api_key: __sdk::TableAppliedDiff<'r, PaymentApiKey>, + payment_app: __sdk::TableAppliedDiff<'r, PaymentApp>, + payment_order: __sdk::TableAppliedDiff<'r, PaymentOrder>, + payment_webhook_delivery: __sdk::TableAppliedDiff<'r, PaymentWebhookDelivery>, profile_code_operation: __sdk::TableAppliedDiff<'r, ProfileCodeOperation>, profile_daily_free_points: __sdk::TableAppliedDiff<'r, ProfileDailyFreePoints>, profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>, @@ -3526,6 +3687,22 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.llm_router_billing_checkpoint, event, ); + callbacks.invoke_table_row_callbacks::( + "payment_api_key", + &self.payment_api_key, + event, + ); + callbacks.invoke_table_row_callbacks::("payment_app", &self.payment_app, event); + callbacks.invoke_table_row_callbacks::( + "payment_order", + &self.payment_order, + event, + ); + callbacks.invoke_table_row_callbacks::( + "payment_webhook_delivery", + &self.payment_webhook_delivery, + event, + ); callbacks.invoke_table_row_callbacks::( "profile_code_operation", &self.profile_code_operation, @@ -4421,6 +4598,10 @@ impl __sdk::SpacetimeModule for RemoteModule { game_distribution_version_table::register_table(client_cache); llm_router_account_table::register_table(client_cache); llm_router_billing_checkpoint_table::register_table(client_cache); + payment_api_key_table::register_table(client_cache); + payment_app_table::register_table(client_cache); + payment_order_table::register_table(client_cache); + payment_webhook_delivery_table::register_table(client_cache); profile_code_operation_table::register_table(client_cache); profile_daily_free_points_table::register_table(client_cache); profile_dashboard_state_table::register_table(client_cache); @@ -4510,6 +4691,10 @@ impl __sdk::SpacetimeModule for RemoteModule { "game_distribution_version", "llm_router_account", "llm_router_billing_checkpoint", + "payment_api_key", + "payment_app", + "payment_order", + "payment_webhook_delivery", "profile_code_operation", "profile_daily_free_points", "profile_dashboard_state", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/authenticate_payment_api_key_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/authenticate_payment_api_key_and_return_procedure.rs new file mode 100644 index 000000000..33c26c311 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/authenticate_payment_api_key_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_api_key_authenticate_input_type::PaymentApiKeyAuthenticateInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct AuthenticatePaymentApiKeyAndReturnArgs { + pub input: PaymentApiKeyAuthenticateInput, +} + +impl __sdk::InModule for AuthenticatePaymentApiKeyAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `authenticate_payment_api_key_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait authenticate_payment_api_key_and_return { + fn authenticate_payment_api_key_and_return(&self, input: PaymentApiKeyAuthenticateInput) { + self.authenticate_payment_api_key_and_return_then(input, |_, _| {}); + } + + fn authenticate_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyAuthenticateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl authenticate_payment_api_key_and_return for super::RemoteProcedures { + fn authenticate_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyAuthenticateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "authenticate_payment_api_key_and_return", + AuthenticatePaymentApiKeyAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_payment_webhooks_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_payment_webhooks_and_return_procedure.rs new file mode 100644 index 000000000..f28ea001f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_payment_webhooks_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_webhook_claim_input_type::PaymentWebhookClaimInput; +use super::payment_webhook_procedure_result_type::PaymentWebhookProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ClaimPaymentWebhooksAndReturnArgs { + pub input: PaymentWebhookClaimInput, +} + +impl __sdk::InModule for ClaimPaymentWebhooksAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `claim_payment_webhooks_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait claim_payment_webhooks_and_return { + fn claim_payment_webhooks_and_return(&self, input: PaymentWebhookClaimInput) { + self.claim_payment_webhooks_and_return_then(input, |_, _| {}); + } + + fn claim_payment_webhooks_and_return_then( + &self, + input: PaymentWebhookClaimInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl claim_payment_webhooks_and_return for super::RemoteProcedures { + fn claim_payment_webhooks_and_return_then( + &self, + input: PaymentWebhookClaimInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentWebhookProcedureResult>( + "claim_payment_webhooks_and_return", + ClaimPaymentWebhooksAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/complete_payment_webhook_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/complete_payment_webhook_and_return_procedure.rs new file mode 100644 index 000000000..8eb38c881 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/complete_payment_webhook_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_webhook_complete_input_type::PaymentWebhookCompleteInput; +use super::payment_webhook_procedure_result_type::PaymentWebhookProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CompletePaymentWebhookAndReturnArgs { + pub input: PaymentWebhookCompleteInput, +} + +impl __sdk::InModule for CompletePaymentWebhookAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `complete_payment_webhook_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait complete_payment_webhook_and_return { + fn complete_payment_webhook_and_return(&self, input: PaymentWebhookCompleteInput) { + self.complete_payment_webhook_and_return_then(input, |_, _| {}); + } + + fn complete_payment_webhook_and_return_then( + &self, + input: PaymentWebhookCompleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl complete_payment_webhook_and_return for super::RemoteProcedures { + fn complete_payment_webhook_and_return_then( + &self, + input: PaymentWebhookCompleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentWebhookProcedureResult>( + "complete_payment_webhook_and_return", + CompletePaymentWebhookAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_payment_api_key_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_api_key_and_return_procedure.rs new file mode 100644 index 000000000..6cd1a0ca5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_api_key_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_api_key_create_input_type::PaymentApiKeyCreateInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreatePaymentApiKeyAndReturnArgs { + pub input: PaymentApiKeyCreateInput, +} + +impl __sdk::InModule for CreatePaymentApiKeyAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_payment_api_key_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_payment_api_key_and_return { + fn create_payment_api_key_and_return(&self, input: PaymentApiKeyCreateInput) { + self.create_payment_api_key_and_return_then(input, |_, _| {}); + } + + fn create_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_payment_api_key_and_return for super::RemoteProcedures { + fn create_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "create_payment_api_key_and_return", + CreatePaymentApiKeyAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_payment_app_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_app_and_return_procedure.rs new file mode 100644 index 000000000..493fba127 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_app_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_app_create_input_type::PaymentAppCreateInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreatePaymentAppAndReturnArgs { + pub input: PaymentAppCreateInput, +} + +impl __sdk::InModule for CreatePaymentAppAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_payment_app_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_payment_app_and_return { + fn create_payment_app_and_return(&self, input: PaymentAppCreateInput) { + self.create_payment_app_and_return_then(input, |_, _| {}); + } + + fn create_payment_app_and_return_then( + &self, + input: PaymentAppCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_payment_app_and_return for super::RemoteProcedures { + fn create_payment_app_and_return_then( + &self, + input: PaymentAppCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "create_payment_app_and_return", + CreatePaymentAppAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_payment_order_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_order_and_return_procedure.rs new file mode 100644 index 000000000..e576d93b7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_payment_order_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_create_input_type::PaymentOrderCreateInput; +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreatePaymentOrderAndReturnArgs { + pub input: PaymentOrderCreateInput, +} + +impl __sdk::InModule for CreatePaymentOrderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_payment_order_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_payment_order_and_return { + fn create_payment_order_and_return(&self, input: PaymentOrderCreateInput) { + self.create_payment_order_and_return_then(input, |_, _| {}); + } + + fn create_payment_order_and_return_then( + &self, + input: PaymentOrderCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_payment_order_and_return for super::RemoteProcedures { + fn create_payment_order_and_return_then( + &self, + input: PaymentOrderCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "create_payment_order_and_return", + CreatePaymentOrderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/enqueue_payment_webhook_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/enqueue_payment_webhook_and_return_procedure.rs new file mode 100644 index 000000000..91c952715 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/enqueue_payment_webhook_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_webhook_enqueue_input_type::PaymentWebhookEnqueueInput; +use super::payment_webhook_procedure_result_type::PaymentWebhookProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct EnqueuePaymentWebhookAndReturnArgs { + pub input: PaymentWebhookEnqueueInput, +} + +impl __sdk::InModule for EnqueuePaymentWebhookAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `enqueue_payment_webhook_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait enqueue_payment_webhook_and_return { + fn enqueue_payment_webhook_and_return(&self, input: PaymentWebhookEnqueueInput) { + self.enqueue_payment_webhook_and_return_then(input, |_, _| {}); + } + + fn enqueue_payment_webhook_and_return_then( + &self, + input: PaymentWebhookEnqueueInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl enqueue_payment_webhook_and_return for super::RemoteProcedures { + fn enqueue_payment_webhook_and_return_then( + &self, + input: PaymentWebhookEnqueueInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentWebhookProcedureResult>( + "enqueue_payment_webhook_and_return", + EnqueuePaymentWebhookAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_and_return_procedure.rs new file mode 100644 index 000000000..981124029 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_get_input_type::PaymentOrderGetInput; +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetPaymentOrderAndReturnArgs { + pub input: PaymentOrderGetInput, +} + +impl __sdk::InModule for GetPaymentOrderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_payment_order_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_payment_order_and_return { + fn get_payment_order_and_return(&self, input: PaymentOrderGetInput) { + self.get_payment_order_and_return_then(input, |_, _| {}); + } + + fn get_payment_order_and_return_then( + &self, + input: PaymentOrderGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_payment_order_and_return for super::RemoteProcedures { + fn get_payment_order_and_return_then( + &self, + input: PaymentOrderGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "get_payment_order_and_return", + GetPaymentOrderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_by_checkout_token_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_by_checkout_token_and_return_procedure.rs new file mode 100644 index 000000000..eb14d8e86 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_payment_order_by_checkout_token_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_checkout_get_input_type::PaymentOrderCheckoutGetInput; +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetPaymentOrderByCheckoutTokenAndReturnArgs { + pub input: PaymentOrderCheckoutGetInput, +} + +impl __sdk::InModule for GetPaymentOrderByCheckoutTokenAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_payment_order_by_checkout_token_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_payment_order_by_checkout_token_and_return { + fn get_payment_order_by_checkout_token_and_return(&self, input: PaymentOrderCheckoutGetInput) { + self.get_payment_order_by_checkout_token_and_return_then(input, |_, _| {}); + } + + fn get_payment_order_by_checkout_token_and_return_then( + &self, + input: PaymentOrderCheckoutGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_payment_order_by_checkout_token_and_return for super::RemoteProcedures { + fn get_payment_order_by_checkout_token_and_return_then( + &self, + input: PaymentOrderCheckoutGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "get_payment_order_by_checkout_token_and_return", + GetPaymentOrderByCheckoutTokenAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_payment_api_keys_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_api_keys_and_return_procedure.rs new file mode 100644 index 000000000..76c904866 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_api_keys_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_api_key_list_input_type::PaymentApiKeyListInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListPaymentApiKeysAndReturnArgs { + pub input: PaymentApiKeyListInput, +} + +impl __sdk::InModule for ListPaymentApiKeysAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_payment_api_keys_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_payment_api_keys_and_return { + fn list_payment_api_keys_and_return(&self, input: PaymentApiKeyListInput) { + self.list_payment_api_keys_and_return_then(input, |_, _| {}); + } + + fn list_payment_api_keys_and_return_then( + &self, + input: PaymentApiKeyListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_payment_api_keys_and_return for super::RemoteProcedures { + fn list_payment_api_keys_and_return_then( + &self, + input: PaymentApiKeyListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "list_payment_api_keys_and_return", + ListPaymentApiKeysAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_payment_apps_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_apps_and_return_procedure.rs new file mode 100644 index 000000000..200701e1e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_apps_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_app_list_input_type::PaymentAppListInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListPaymentAppsAndReturnArgs { + pub input: PaymentAppListInput, +} + +impl __sdk::InModule for ListPaymentAppsAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_payment_apps_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_payment_apps_and_return { + fn list_payment_apps_and_return(&self, input: PaymentAppListInput) { + self.list_payment_apps_and_return_then(input, |_, _| {}); + } + + fn list_payment_apps_and_return_then( + &self, + input: PaymentAppListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_payment_apps_and_return for super::RemoteProcedures { + fn list_payment_apps_and_return_then( + &self, + input: PaymentAppListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "list_payment_apps_and_return", + ListPaymentAppsAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_payment_orders_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_orders_and_return_procedure.rs new file mode 100644 index 000000000..f00f5674d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_payment_orders_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_list_input_type::PaymentOrderListInput; +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListPaymentOrdersAndReturnArgs { + pub input: PaymentOrderListInput, +} + +impl __sdk::InModule for ListPaymentOrdersAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_payment_orders_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_payment_orders_and_return { + fn list_payment_orders_and_return(&self, input: PaymentOrderListInput) { + self.list_payment_orders_and_return_then(input, |_, _| {}); + } + + fn list_payment_orders_and_return_then( + &self, + input: PaymentOrderListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_payment_orders_and_return for super::RemoteProcedures { + fn list_payment_orders_and_return_then( + &self, + input: PaymentOrderListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "list_payment_orders_and_return", + ListPaymentOrdersAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_paid_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_paid_and_return_procedure.rs new file mode 100644 index 000000000..15600aaff --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_paid_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_mark_paid_input_type::PaymentOrderMarkPaidInput; +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct MarkPaymentOrderPaidAndReturnArgs { + pub input: PaymentOrderMarkPaidInput, +} + +impl __sdk::InModule for MarkPaymentOrderPaidAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `mark_payment_order_paid_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait mark_payment_order_paid_and_return { + fn mark_payment_order_paid_and_return(&self, input: PaymentOrderMarkPaidInput) { + self.mark_payment_order_paid_and_return_then(input, |_, _| {}); + } + + fn mark_payment_order_paid_and_return_then( + &self, + input: PaymentOrderMarkPaidInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl mark_payment_order_paid_and_return for super::RemoteProcedures { + fn mark_payment_order_paid_and_return_then( + &self, + input: PaymentOrderMarkPaidInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "mark_payment_order_paid_and_return", + MarkPaymentOrderPaidAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_provider_attempted_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_provider_attempted_and_return_procedure.rs new file mode 100644 index 000000000..b533220a7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/mark_payment_order_provider_attempted_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; +use super::payment_order_provider_attempt_input_type::PaymentOrderProviderAttemptInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct MarkPaymentOrderProviderAttemptedAndReturnArgs { + pub input: PaymentOrderProviderAttemptInput, +} + +impl __sdk::InModule for MarkPaymentOrderProviderAttemptedAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `mark_payment_order_provider_attempted_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait mark_payment_order_provider_attempted_and_return { + fn mark_payment_order_provider_attempted_and_return( + &self, + input: PaymentOrderProviderAttemptInput, + ) { + self.mark_payment_order_provider_attempted_and_return_then(input, |_, _| {}); + } + + fn mark_payment_order_provider_attempted_and_return_then( + &self, + input: PaymentOrderProviderAttemptInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl mark_payment_order_provider_attempted_and_return for super::RemoteProcedures { + fn mark_payment_order_provider_attempted_and_return_then( + &self, + input: PaymentOrderProviderAttemptInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "mark_payment_order_provider_attempted_and_return", + MarkPaymentOrderProviderAttemptedAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_authenticate_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_authenticate_input_type.rs new file mode 100644 index 000000000..e875b26e1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_authenticate_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKeyAuthenticateInput { + pub key_hash: String, + pub used_at_micros: i64, +} + +impl __sdk::InModule for PaymentApiKeyAuthenticateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_create_input_type.rs new file mode 100644 index 000000000..f197e8a9b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_create_input_type.rs @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKeyCreateInput { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub key_hash: String, + pub scopes_json: String, + pub now_micros: i64, +} + +impl __sdk::InModule for PaymentApiKeyCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_list_input_type.rs new file mode 100644 index 000000000..71c0c77aa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_list_input_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKeyListInput { + pub owner_user_id: String, +} + +impl __sdk::InModule for PaymentApiKeyListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_revoke_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_revoke_input_type.rs new file mode 100644 index 000000000..0078ab788 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_revoke_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKeyRevokeInput { + pub key_id: String, + pub owner_user_id: String, + pub revoked_at_micros: i64, +} + +impl __sdk::InModule for PaymentApiKeyRevokeInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_snapshot_type.rs new file mode 100644 index 000000000..fc27bf52d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_snapshot_type.rs @@ -0,0 +1,24 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKeySnapshot { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub scopes_json: String, + pub created_at_micros: i64, + pub last_used_at_micros: Option, + pub revoked_at_micros: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentApiKeySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_table.rs new file mode 100644 index 000000000..409f34921 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_table.rs @@ -0,0 +1,259 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::payment_api_key_type::PaymentApiKey; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `payment_api_key`. +/// +/// Obtain a handle from the [`PaymentApiKeyTableAccess::payment_api_key`] method on [`super::RemoteTables`], +/// like `ctx.db.payment_api_key()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_api_key().on_insert(...)`. +pub struct PaymentApiKeyTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +/// Lifetime-aware accessor marker for the table `payment_api_key`. +pub struct PaymentApiKeyTableAccessor; + +impl __sdk::TableAccessor for PaymentApiKeyTableAccessor { + type Row = PaymentApiKey; + type Handle<'db> = PaymentApiKeyTableHandle<'db>; + + fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> { + db.payment_api_key() + } +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `payment_api_key`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PaymentApiKeyTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PaymentApiKeyTableHandle`], which mediates access to the table `payment_api_key`. + fn payment_api_key(&self) -> PaymentApiKeyTableHandle<'_>; +} + +impl PaymentApiKeyTableAccess for super::RemoteTables { + fn payment_api_key(&self) -> PaymentApiKeyTableHandle<'_> { + PaymentApiKeyTableHandle { + imp: self.imp.get_table::("payment_api_key"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PaymentApiKeyInsertCallbackId(__sdk::CallbackId); +pub struct PaymentApiKeyDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableLike for PaymentApiKeyTableHandle<'ctx> { + type Row = PaymentApiKey; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } +} + +impl<'ctx> __sdk::Table for PaymentApiKeyTableHandle<'ctx> { + type Row = PaymentApiKey; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PaymentApiKeyInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyInsertCallbackId { + PaymentApiKeyInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentApiKeyInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PaymentApiKeyDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyDeleteCallbackId { + PaymentApiKeyDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentApiKeyDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +impl<'ctx> __sdk::WithInsert for PaymentApiKeyTableHandle<'ctx> { + type InsertCallbackId = PaymentApiKeyInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyInsertCallbackId { + PaymentApiKeyInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentApiKeyInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } +} + +impl<'ctx> __sdk::WithDelete for PaymentApiKeyTableHandle<'ctx> { + type DeleteCallbackId = PaymentApiKeyDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyDeleteCallbackId { + PaymentApiKeyDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentApiKeyDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PaymentApiKeyUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PaymentApiKeyTableHandle<'ctx> { + type UpdateCallbackId = PaymentApiKeyUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyUpdateCallbackId { + PaymentApiKeyUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentApiKeyUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +impl<'ctx> __sdk::WithUpdate for PaymentApiKeyTableHandle<'ctx> { + type UpdateCallbackId = PaymentApiKeyUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentApiKeyUpdateCallbackId { + PaymentApiKeyUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentApiKeyUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `key_id` unique index on the table `payment_api_key`, +/// which allows point queries on the field of the same name +/// via the [`PaymentApiKeyKeyIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_api_key().key_id().find(...)`. +pub struct PaymentApiKeyKeyIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentApiKeyTableHandle<'ctx> { + /// Get a handle on the `key_id` unique index on the table `payment_api_key`. + pub fn key_id(&self) -> PaymentApiKeyKeyIdUnique<'ctx> { + PaymentApiKeyKeyIdUnique { + imp: self.imp.get_unique_constraint::("key_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentApiKeyKeyIdUnique<'ctx> { + /// Find the subscribed row whose `key_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +/// Access to the `key_hash` unique index on the table `payment_api_key`, +/// which allows point queries on the field of the same name +/// via the [`PaymentApiKeyKeyHashUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_api_key().key_hash().find(...)`. +pub struct PaymentApiKeyKeyHashUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentApiKeyTableHandle<'ctx> { + /// Get a handle on the `key_hash` unique index on the table `payment_api_key`. + pub fn key_hash(&self) -> PaymentApiKeyKeyHashUnique<'ctx> { + PaymentApiKeyKeyHashUnique { + imp: self.imp.get_unique_constraint::("key_hash"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentApiKeyKeyHashUnique<'ctx> { + /// Find the subscribed row whose `key_hash` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("payment_api_key"); + _table.add_unique_constraint::("key_id", |row| &row.key_id); + _table.add_unique_constraint::("key_hash", |row| &row.key_hash); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `PaymentApiKey`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait payment_api_keyQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PaymentApiKey`. + fn payment_api_key(&self) -> __sdk::__query_builder::Table; +} + +impl payment_api_keyQueryTableAccess for __sdk::QueryTableAccessor { + fn payment_api_key(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("payment_api_key") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_type.rs new file mode 100644 index 000000000..4a9bc51e7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_api_key_type.rs @@ -0,0 +1,85 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApiKey { + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + pub key_hash: String, + pub scopes_json: String, + pub created_at: __sdk::Timestamp, + pub last_used_at: Option<__sdk::Timestamp>, + pub revoked_at: Option<__sdk::Timestamp>, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for PaymentApiKey { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PaymentApiKey`. +/// +/// Provides typed access to columns for query building. +pub struct PaymentApiKeyCols { + pub key_id: __sdk::__query_builder::Col, + pub app_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, + pub key_prefix: __sdk::__query_builder::Col, + pub key_hash: __sdk::__query_builder::Col, + pub scopes_json: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub last_used_at: __sdk::__query_builder::Col>, + pub revoked_at: __sdk::__query_builder::Col>, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for PaymentApiKey { + type Cols = PaymentApiKeyCols; + fn cols(table_name: &'static str) -> Self::Cols { + PaymentApiKeyCols { + key_id: __sdk::__query_builder::Col::new(table_name, "key_id"), + app_id: __sdk::__query_builder::Col::new(table_name, "app_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + key_prefix: __sdk::__query_builder::Col::new(table_name, "key_prefix"), + key_hash: __sdk::__query_builder::Col::new(table_name, "key_hash"), + scopes_json: __sdk::__query_builder::Col::new(table_name, "scopes_json"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + last_used_at: __sdk::__query_builder::Col::new(table_name, "last_used_at"), + revoked_at: __sdk::__query_builder::Col::new(table_name, "revoked_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `PaymentApiKey`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PaymentApiKeyIxCols { + pub app_id: __sdk::__query_builder::IxCol, + pub key_hash: __sdk::__query_builder::IxCol, + pub key_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PaymentApiKey { + type IxCols = PaymentApiKeyIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PaymentApiKeyIxCols { + app_id: __sdk::__query_builder::IxCol::new(table_name, "app_id"), + key_hash: __sdk::__query_builder::IxCol::new(table_name, "key_hash"), + key_id: __sdk::__query_builder::IxCol::new(table_name, "key_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PaymentApiKey {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_create_input_type.rs new file mode 100644 index 000000000..ffbc34df5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_create_input_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentAppCreateInput { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub now_micros: i64, +} + +impl __sdk::InModule for PaymentAppCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_list_input_type.rs new file mode 100644 index 000000000..641a4020d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_list_input_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentAppListInput { + pub owner_user_id: String, +} + +impl __sdk::InModule for PaymentAppListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_procedure_result_type.rs new file mode 100644 index 000000000..2bbb8ef04 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_procedure_result_type.rs @@ -0,0 +1,23 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_api_key_snapshot_type::PaymentApiKeySnapshot; +use super::payment_app_snapshot_type::PaymentAppSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentAppProcedureResult { + pub ok: bool, + pub app: Option, + pub key: Option, + pub apps: Vec, + pub keys: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for PaymentAppProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_snapshot_type.rs new file mode 100644 index 000000000..a96a682da --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_snapshot_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentAppSnapshot { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub enabled: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentAppSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_table.rs new file mode 100644 index 000000000..74fea42c5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_table.rs @@ -0,0 +1,228 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::payment_app_type::PaymentApp; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `payment_app`. +/// +/// Obtain a handle from the [`PaymentAppTableAccess::payment_app`] method on [`super::RemoteTables`], +/// like `ctx.db.payment_app()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_app().on_insert(...)`. +pub struct PaymentAppTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +/// Lifetime-aware accessor marker for the table `payment_app`. +pub struct PaymentAppTableAccessor; + +impl __sdk::TableAccessor for PaymentAppTableAccessor { + type Row = PaymentApp; + type Handle<'db> = PaymentAppTableHandle<'db>; + + fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> { + db.payment_app() + } +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `payment_app`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PaymentAppTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PaymentAppTableHandle`], which mediates access to the table `payment_app`. + fn payment_app(&self) -> PaymentAppTableHandle<'_>; +} + +impl PaymentAppTableAccess for super::RemoteTables { + fn payment_app(&self) -> PaymentAppTableHandle<'_> { + PaymentAppTableHandle { + imp: self.imp.get_table::("payment_app"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PaymentAppInsertCallbackId(__sdk::CallbackId); +pub struct PaymentAppDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableLike for PaymentAppTableHandle<'ctx> { + type Row = PaymentApp; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } +} + +impl<'ctx> __sdk::Table for PaymentAppTableHandle<'ctx> { + type Row = PaymentApp; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PaymentAppInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentAppInsertCallbackId { + PaymentAppInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentAppInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PaymentAppDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentAppDeleteCallbackId { + PaymentAppDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentAppDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +impl<'ctx> __sdk::WithInsert for PaymentAppTableHandle<'ctx> { + type InsertCallbackId = PaymentAppInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentAppInsertCallbackId { + PaymentAppInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentAppInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } +} + +impl<'ctx> __sdk::WithDelete for PaymentAppTableHandle<'ctx> { + type DeleteCallbackId = PaymentAppDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentAppDeleteCallbackId { + PaymentAppDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentAppDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PaymentAppUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PaymentAppTableHandle<'ctx> { + type UpdateCallbackId = PaymentAppUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentAppUpdateCallbackId { + PaymentAppUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentAppUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +impl<'ctx> __sdk::WithUpdate for PaymentAppTableHandle<'ctx> { + type UpdateCallbackId = PaymentAppUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentAppUpdateCallbackId { + PaymentAppUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentAppUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `app_id` unique index on the table `payment_app`, +/// which allows point queries on the field of the same name +/// via the [`PaymentAppAppIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_app().app_id().find(...)`. +pub struct PaymentAppAppIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentAppTableHandle<'ctx> { + /// Get a handle on the `app_id` unique index on the table `payment_app`. + pub fn app_id(&self) -> PaymentAppAppIdUnique<'ctx> { + PaymentAppAppIdUnique { + imp: self.imp.get_unique_constraint::("app_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentAppAppIdUnique<'ctx> { + /// Find the subscribed row whose `app_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("payment_app"); + _table.add_unique_constraint::("app_id", |row| &row.app_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `PaymentApp`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait payment_appQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PaymentApp`. + fn payment_app(&self) -> __sdk::__query_builder::Table; +} + +impl payment_appQueryTableAccess for __sdk::QueryTableAccessor { + fn payment_app(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("payment_app") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_type.rs new file mode 100644 index 000000000..f34b81a07 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_type.rs @@ -0,0 +1,69 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentApp { + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub enabled: bool, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for PaymentApp { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PaymentApp`. +/// +/// Provides typed access to columns for query building. +pub struct PaymentAppCols { + pub app_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, + pub callback_url: __sdk::__query_builder::Col>, + pub enabled: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for PaymentApp { + type Cols = PaymentAppCols; + fn cols(table_name: &'static str) -> Self::Cols { + PaymentAppCols { + app_id: __sdk::__query_builder::Col::new(table_name, "app_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + callback_url: __sdk::__query_builder::Col::new(table_name, "callback_url"), + enabled: __sdk::__query_builder::Col::new(table_name, "enabled"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `PaymentApp`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PaymentAppIxCols { + pub app_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PaymentApp { + type IxCols = PaymentAppIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PaymentAppIxCols { + app_id: __sdk::__query_builder::IxCol::new(table_name, "app_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PaymentApp {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_app_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_update_input_type.rs new file mode 100644 index 000000000..f83e5f760 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_app_update_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentAppUpdateInput { + pub app_id: String, + pub owner_user_id: String, + pub enabled: bool, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentAppUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_checkout_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_checkout_get_input_type.rs new file mode 100644 index 000000000..66b87f70e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_checkout_get_input_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderCheckoutGetInput { + pub checkout_token: String, +} + +impl __sdk::InModule for PaymentOrderCheckoutGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_create_input_type.rs new file mode 100644 index 000000000..a9952746d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_create_input_type.rs @@ -0,0 +1,31 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_provider_type::PaymentProvider; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderCreateInput { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub checkout_token: String, + pub now_micros: i64, + pub expires_at_micros: i64, + pub callback_url: Option, + pub callback_signing_key_hash: Option, +} + +impl __sdk::InModule for PaymentOrderCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_get_input_type.rs new file mode 100644 index 000000000..203c66ccb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_get_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderGetInput { + pub order_id: String, + pub owner_user_id: Option, +} + +impl __sdk::InModule for PaymentOrderGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_list_input_type.rs new file mode 100644 index 000000000..2be50fede --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_list_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderListInput { + pub app_id: Option, + pub owner_user_id: String, + pub limit: u32, +} + +impl __sdk::InModule for PaymentOrderListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_mark_paid_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_mark_paid_input_type.rs new file mode 100644 index 000000000..837ac27f6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_mark_paid_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderMarkPaidInput { + pub order_id: String, + pub provider_trade_no: String, + pub amount_cents: u64, + pub paid_at_micros: i64, +} + +impl __sdk::InModule for PaymentOrderMarkPaidInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_procedure_result_type.rs new file mode 100644 index 000000000..22c3f85af --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_procedure_result_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_snapshot_type::PaymentOrderSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderProcedureResult { + pub ok: bool, + pub order: Option, + pub orders: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for PaymentOrderProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_attempt_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_attempt_input_type.rs new file mode 100644 index 000000000..e2b0002bb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_attempt_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderProviderAttemptInput { + pub order_id: String, + pub attempted_at_micros: i64, +} + +impl __sdk::InModule for PaymentOrderProviderAttemptInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_code_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_code_input_type.rs new file mode 100644 index 000000000..b02d63297 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_provider_code_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderProviderCodeInput { + pub order_id: String, + pub provider_code_url: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentOrderProviderCodeInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_snapshot_type.rs new file mode 100644 index 000000000..f2e351627 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_snapshot_type.rs @@ -0,0 +1,38 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_status_type::PaymentOrderStatus; +use super::payment_provider_type::PaymentProvider; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrderSnapshot { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub provider_trade_no: Option, + pub provider_code_url: Option, + pub callback_url: Option, + pub callback_signing_key_hash: Option, + pub provider_attempted_at_micros: Option, + pub checkout_token: String, + pub status: PaymentOrderStatus, + pub created_at_micros: i64, + pub expires_at_micros: i64, + pub paid_at_micros: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentOrderSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_status_type.rs new file mode 100644 index 000000000..046f3370c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_status_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum PaymentOrderStatus { + Pending, + + Paying, + + Paid, + + Expired, + + Closed, + + Refunded, +} + +impl __sdk::InModule for PaymentOrderStatus { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_table.rs new file mode 100644 index 000000000..58f65872a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_table.rs @@ -0,0 +1,328 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::payment_order_status_type::PaymentOrderStatus; +use super::payment_order_type::PaymentOrder; +use super::payment_provider_type::PaymentProvider; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `payment_order`. +/// +/// Obtain a handle from the [`PaymentOrderTableAccess::payment_order`] method on [`super::RemoteTables`], +/// like `ctx.db.payment_order()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_order().on_insert(...)`. +pub struct PaymentOrderTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +/// Lifetime-aware accessor marker for the table `payment_order`. +pub struct PaymentOrderTableAccessor; + +impl __sdk::TableAccessor for PaymentOrderTableAccessor { + type Row = PaymentOrder; + type Handle<'db> = PaymentOrderTableHandle<'db>; + + fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> { + db.payment_order() + } +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `payment_order`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PaymentOrderTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PaymentOrderTableHandle`], which mediates access to the table `payment_order`. + fn payment_order(&self) -> PaymentOrderTableHandle<'_>; +} + +impl PaymentOrderTableAccess for super::RemoteTables { + fn payment_order(&self) -> PaymentOrderTableHandle<'_> { + PaymentOrderTableHandle { + imp: self.imp.get_table::("payment_order"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PaymentOrderInsertCallbackId(__sdk::CallbackId); +pub struct PaymentOrderDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableLike for PaymentOrderTableHandle<'ctx> { + type Row = PaymentOrder; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } +} + +impl<'ctx> __sdk::Table for PaymentOrderTableHandle<'ctx> { + type Row = PaymentOrder; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PaymentOrderInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentOrderInsertCallbackId { + PaymentOrderInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentOrderInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PaymentOrderDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentOrderDeleteCallbackId { + PaymentOrderDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentOrderDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +impl<'ctx> __sdk::WithInsert for PaymentOrderTableHandle<'ctx> { + type InsertCallbackId = PaymentOrderInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentOrderInsertCallbackId { + PaymentOrderInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentOrderInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } +} + +impl<'ctx> __sdk::WithDelete for PaymentOrderTableHandle<'ctx> { + type DeleteCallbackId = PaymentOrderDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentOrderDeleteCallbackId { + PaymentOrderDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentOrderDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PaymentOrderUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PaymentOrderTableHandle<'ctx> { + type UpdateCallbackId = PaymentOrderUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentOrderUpdateCallbackId { + PaymentOrderUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentOrderUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +impl<'ctx> __sdk::WithUpdate for PaymentOrderTableHandle<'ctx> { + type UpdateCallbackId = PaymentOrderUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentOrderUpdateCallbackId { + PaymentOrderUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentOrderUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `order_id` unique index on the table `payment_order`, +/// which allows point queries on the field of the same name +/// via the [`PaymentOrderOrderIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_order().order_id().find(...)`. +pub struct PaymentOrderOrderIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentOrderTableHandle<'ctx> { + /// Get a handle on the `order_id` unique index on the table `payment_order`. + pub fn order_id(&self) -> PaymentOrderOrderIdUnique<'ctx> { + PaymentOrderOrderIdUnique { + imp: self.imp.get_unique_constraint::("order_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentOrderOrderIdUnique<'ctx> { + /// Find the subscribed row whose `order_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +/// Access to the `idempotency_lookup` unique index on the table `payment_order`, +/// which allows point queries on the field of the same name +/// via the [`PaymentOrderIdempotencyLookupUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_order().idempotency_lookup().find(...)`. +pub struct PaymentOrderIdempotencyLookupUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentOrderTableHandle<'ctx> { + /// Get a handle on the `idempotency_lookup` unique index on the table `payment_order`. + pub fn idempotency_lookup(&self) -> PaymentOrderIdempotencyLookupUnique<'ctx> { + PaymentOrderIdempotencyLookupUnique { + imp: self + .imp + .get_unique_constraint::("idempotency_lookup"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentOrderIdempotencyLookupUnique<'ctx> { + /// Find the subscribed row whose `idempotency_lookup` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +/// Access to the `merchant_order_lookup` unique index on the table `payment_order`, +/// which allows point queries on the field of the same name +/// via the [`PaymentOrderMerchantOrderLookupUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_order().merchant_order_lookup().find(...)`. +pub struct PaymentOrderMerchantOrderLookupUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentOrderTableHandle<'ctx> { + /// Get a handle on the `merchant_order_lookup` unique index on the table `payment_order`. + pub fn merchant_order_lookup(&self) -> PaymentOrderMerchantOrderLookupUnique<'ctx> { + PaymentOrderMerchantOrderLookupUnique { + imp: self + .imp + .get_unique_constraint::("merchant_order_lookup"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentOrderMerchantOrderLookupUnique<'ctx> { + /// Find the subscribed row whose `merchant_order_lookup` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +/// Access to the `checkout_token` unique index on the table `payment_order`, +/// which allows point queries on the field of the same name +/// via the [`PaymentOrderCheckoutTokenUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_order().checkout_token().find(...)`. +pub struct PaymentOrderCheckoutTokenUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentOrderTableHandle<'ctx> { + /// Get a handle on the `checkout_token` unique index on the table `payment_order`. + pub fn checkout_token(&self) -> PaymentOrderCheckoutTokenUnique<'ctx> { + PaymentOrderCheckoutTokenUnique { + imp: self.imp.get_unique_constraint::("checkout_token"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentOrderCheckoutTokenUnique<'ctx> { + /// Find the subscribed row whose `checkout_token` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("payment_order"); + _table.add_unique_constraint::("order_id", |row| &row.order_id); + _table.add_unique_constraint::("idempotency_lookup", |row| &row.idempotency_lookup); + _table + .add_unique_constraint::("merchant_order_lookup", |row| &row.merchant_order_lookup); + _table.add_unique_constraint::("checkout_token", |row| &row.checkout_token); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `PaymentOrder`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait payment_orderQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PaymentOrder`. + fn payment_order(&self) -> __sdk::__query_builder::Table; +} + +impl payment_orderQueryTableAccess for __sdk::QueryTableAccessor { + fn payment_order(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("payment_order") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_order_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_type.rs new file mode 100644 index 000000000..abb46c6d5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_order_type.rs @@ -0,0 +1,142 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_status_type::PaymentOrderStatus; +use super::payment_provider_type::PaymentProvider; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentOrder { + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + pub idempotency_lookup: String, + pub merchant_order_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub provider_trade_no: Option, + pub checkout_token: String, + pub status: PaymentOrderStatus, + pub created_at: __sdk::Timestamp, + pub expires_at: __sdk::Timestamp, + pub paid_at: Option<__sdk::Timestamp>, + pub updated_at: __sdk::Timestamp, + pub provider_code_url: Option, + pub callback_url: Option, + pub callback_signing_key_hash: Option, + pub provider_attempted_at: Option<__sdk::Timestamp>, +} + +impl __sdk::InModule for PaymentOrder { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PaymentOrder`. +/// +/// Provides typed access to columns for query building. +pub struct PaymentOrderCols { + pub order_id: __sdk::__query_builder::Col, + pub app_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub merchant_order_id: __sdk::__query_builder::Col, + pub idempotency_lookup: __sdk::__query_builder::Col, + pub merchant_order_lookup: __sdk::__query_builder::Col, + pub title: __sdk::__query_builder::Col, + pub items_json: __sdk::__query_builder::Col, + pub amount_cents: __sdk::__query_builder::Col, + pub currency: __sdk::__query_builder::Col, + pub provider: __sdk::__query_builder::Col, + pub provider_trade_no: __sdk::__query_builder::Col>, + pub checkout_token: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub expires_at: __sdk::__query_builder::Col, + pub paid_at: __sdk::__query_builder::Col>, + pub updated_at: __sdk::__query_builder::Col, + pub provider_code_url: __sdk::__query_builder::Col>, + pub callback_url: __sdk::__query_builder::Col>, + pub callback_signing_key_hash: __sdk::__query_builder::Col>, + pub provider_attempted_at: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for PaymentOrder { + type Cols = PaymentOrderCols; + fn cols(table_name: &'static str) -> Self::Cols { + PaymentOrderCols { + order_id: __sdk::__query_builder::Col::new(table_name, "order_id"), + app_id: __sdk::__query_builder::Col::new(table_name, "app_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + merchant_order_id: __sdk::__query_builder::Col::new(table_name, "merchant_order_id"), + idempotency_lookup: __sdk::__query_builder::Col::new(table_name, "idempotency_lookup"), + merchant_order_lookup: __sdk::__query_builder::Col::new( + table_name, + "merchant_order_lookup", + ), + title: __sdk::__query_builder::Col::new(table_name, "title"), + items_json: __sdk::__query_builder::Col::new(table_name, "items_json"), + amount_cents: __sdk::__query_builder::Col::new(table_name, "amount_cents"), + currency: __sdk::__query_builder::Col::new(table_name, "currency"), + provider: __sdk::__query_builder::Col::new(table_name, "provider"), + provider_trade_no: __sdk::__query_builder::Col::new(table_name, "provider_trade_no"), + checkout_token: __sdk::__query_builder::Col::new(table_name, "checkout_token"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + expires_at: __sdk::__query_builder::Col::new(table_name, "expires_at"), + paid_at: __sdk::__query_builder::Col::new(table_name, "paid_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + provider_code_url: __sdk::__query_builder::Col::new(table_name, "provider_code_url"), + callback_url: __sdk::__query_builder::Col::new(table_name, "callback_url"), + callback_signing_key_hash: __sdk::__query_builder::Col::new( + table_name, + "callback_signing_key_hash", + ), + provider_attempted_at: __sdk::__query_builder::Col::new( + table_name, + "provider_attempted_at", + ), + } + } +} + +/// Indexed column accessor struct for the table `PaymentOrder`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PaymentOrderIxCols { + pub app_id: __sdk::__query_builder::IxCol, + pub checkout_token: __sdk::__query_builder::IxCol, + pub idempotency_lookup: __sdk::__query_builder::IxCol, + pub merchant_order_lookup: __sdk::__query_builder::IxCol, + pub order_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, + pub status: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PaymentOrder { + type IxCols = PaymentOrderIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PaymentOrderIxCols { + app_id: __sdk::__query_builder::IxCol::new(table_name, "app_id"), + checkout_token: __sdk::__query_builder::IxCol::new(table_name, "checkout_token"), + idempotency_lookup: __sdk::__query_builder::IxCol::new( + table_name, + "idempotency_lookup", + ), + merchant_order_lookup: __sdk::__query_builder::IxCol::new( + table_name, + "merchant_order_lookup", + ), + order_id: __sdk::__query_builder::IxCol::new(table_name, "order_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + status: __sdk::__query_builder::IxCol::new(table_name, "status"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PaymentOrder {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_provider_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_provider_type.rs new file mode 100644 index 000000000..c006a51ca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_provider_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum PaymentProvider { + WechatNative, +} + +impl __sdk::InModule for PaymentProvider { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_claim_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_claim_input_type.rs new file mode 100644 index 000000000..3fdb121b4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_claim_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookClaimInput { + pub worker_id: String, + pub limit: u32, + pub now_micros: i64, + pub lease_expires_at_micros: i64, +} + +impl __sdk::InModule for PaymentWebhookClaimInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_complete_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_complete_input_type.rs new file mode 100644 index 000000000..ecbdd90dd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_complete_input_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookCompleteInput { + pub delivery_id: String, + pub worker_id: String, + pub success: bool, + pub error_message: Option, + pub now_micros: i64, + pub next_available_at_micros: i64, + pub attempt_count: u32, +} + +impl __sdk::InModule for PaymentWebhookCompleteInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_snapshot_type.rs new file mode 100644 index 000000000..22eca80db --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_snapshot_type.rs @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookDeliverySnapshot { + pub delivery_id: String, + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub callback_url: String, + pub payload_json: String, + pub signature: String, + pub status: String, + pub attempt_count: u32, + pub available_at_micros: i64, + pub lease_worker_id: Option, + pub lease_expires_at_micros: Option, + pub last_error_message: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PaymentWebhookDeliverySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_table.rs new file mode 100644 index 000000000..8cc76e053 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_table.rs @@ -0,0 +1,231 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::payment_webhook_delivery_type::PaymentWebhookDelivery; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `payment_webhook_delivery`. +/// +/// Obtain a handle from the [`PaymentWebhookDeliveryTableAccess::payment_webhook_delivery`] method on [`super::RemoteTables`], +/// like `ctx.db.payment_webhook_delivery()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_webhook_delivery().on_insert(...)`. +pub struct PaymentWebhookDeliveryTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +/// Lifetime-aware accessor marker for the table `payment_webhook_delivery`. +pub struct PaymentWebhookDeliveryTableAccessor; + +impl __sdk::TableAccessor for PaymentWebhookDeliveryTableAccessor { + type Row = PaymentWebhookDelivery; + type Handle<'db> = PaymentWebhookDeliveryTableHandle<'db>; + + fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> { + db.payment_webhook_delivery() + } +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `payment_webhook_delivery`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PaymentWebhookDeliveryTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PaymentWebhookDeliveryTableHandle`], which mediates access to the table `payment_webhook_delivery`. + fn payment_webhook_delivery(&self) -> PaymentWebhookDeliveryTableHandle<'_>; +} + +impl PaymentWebhookDeliveryTableAccess for super::RemoteTables { + fn payment_webhook_delivery(&self) -> PaymentWebhookDeliveryTableHandle<'_> { + PaymentWebhookDeliveryTableHandle { + imp: self + .imp + .get_table::("payment_webhook_delivery"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PaymentWebhookDeliveryInsertCallbackId(__sdk::CallbackId); +pub struct PaymentWebhookDeliveryDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableLike for PaymentWebhookDeliveryTableHandle<'ctx> { + type Row = PaymentWebhookDelivery; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } +} + +impl<'ctx> __sdk::Table for PaymentWebhookDeliveryTableHandle<'ctx> { + type Row = PaymentWebhookDelivery; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PaymentWebhookDeliveryInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryInsertCallbackId { + PaymentWebhookDeliveryInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentWebhookDeliveryInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PaymentWebhookDeliveryDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryDeleteCallbackId { + PaymentWebhookDeliveryDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentWebhookDeliveryDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +impl<'ctx> __sdk::WithInsert for PaymentWebhookDeliveryTableHandle<'ctx> { + type InsertCallbackId = PaymentWebhookDeliveryInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryInsertCallbackId { + PaymentWebhookDeliveryInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PaymentWebhookDeliveryInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } +} + +impl<'ctx> __sdk::WithDelete for PaymentWebhookDeliveryTableHandle<'ctx> { + type DeleteCallbackId = PaymentWebhookDeliveryDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryDeleteCallbackId { + PaymentWebhookDeliveryDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PaymentWebhookDeliveryDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PaymentWebhookDeliveryUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PaymentWebhookDeliveryTableHandle<'ctx> { + type UpdateCallbackId = PaymentWebhookDeliveryUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryUpdateCallbackId { + PaymentWebhookDeliveryUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentWebhookDeliveryUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +impl<'ctx> __sdk::WithUpdate for PaymentWebhookDeliveryTableHandle<'ctx> { + type UpdateCallbackId = PaymentWebhookDeliveryUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PaymentWebhookDeliveryUpdateCallbackId { + PaymentWebhookDeliveryUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PaymentWebhookDeliveryUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `delivery_id` unique index on the table `payment_webhook_delivery`, +/// which allows point queries on the field of the same name +/// via the [`PaymentWebhookDeliveryDeliveryIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.payment_webhook_delivery().delivery_id().find(...)`. +pub struct PaymentWebhookDeliveryDeliveryIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PaymentWebhookDeliveryTableHandle<'ctx> { + /// Get a handle on the `delivery_id` unique index on the table `payment_webhook_delivery`. + pub fn delivery_id(&self) -> PaymentWebhookDeliveryDeliveryIdUnique<'ctx> { + PaymentWebhookDeliveryDeliveryIdUnique { + imp: self.imp.get_unique_constraint::("delivery_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PaymentWebhookDeliveryDeliveryIdUnique<'ctx> { + /// Find the subscribed row whose `delivery_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("payment_webhook_delivery"); + _table.add_unique_constraint::("delivery_id", |row| &row.delivery_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `PaymentWebhookDelivery`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait payment_webhook_deliveryQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PaymentWebhookDelivery`. + fn payment_webhook_delivery(&self) -> __sdk::__query_builder::Table; +} + +impl payment_webhook_deliveryQueryTableAccess for __sdk::QueryTableAccessor { + fn payment_webhook_delivery(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("payment_webhook_delivery") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_type.rs new file mode 100644 index 000000000..be33d532c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_delivery_type.rs @@ -0,0 +1,96 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookDelivery { + pub delivery_id: String, + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub callback_url: String, + pub payload_json: String, + pub signature: String, + pub status: String, + pub attempt_count: u32, + pub available_at: __sdk::Timestamp, + pub lease_worker_id: Option, + pub lease_expires_at: Option<__sdk::Timestamp>, + pub last_error_message: Option, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for PaymentWebhookDelivery { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PaymentWebhookDelivery`. +/// +/// Provides typed access to columns for query building. +pub struct PaymentWebhookDeliveryCols { + pub delivery_id: __sdk::__query_builder::Col, + pub order_id: __sdk::__query_builder::Col, + pub app_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub callback_url: __sdk::__query_builder::Col, + pub payload_json: __sdk::__query_builder::Col, + pub signature: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub attempt_count: __sdk::__query_builder::Col, + pub available_at: __sdk::__query_builder::Col, + pub lease_worker_id: __sdk::__query_builder::Col>, + pub lease_expires_at: + __sdk::__query_builder::Col>, + pub last_error_message: __sdk::__query_builder::Col>, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for PaymentWebhookDelivery { + type Cols = PaymentWebhookDeliveryCols; + fn cols(table_name: &'static str) -> Self::Cols { + PaymentWebhookDeliveryCols { + delivery_id: __sdk::__query_builder::Col::new(table_name, "delivery_id"), + order_id: __sdk::__query_builder::Col::new(table_name, "order_id"), + app_id: __sdk::__query_builder::Col::new(table_name, "app_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + callback_url: __sdk::__query_builder::Col::new(table_name, "callback_url"), + payload_json: __sdk::__query_builder::Col::new(table_name, "payload_json"), + signature: __sdk::__query_builder::Col::new(table_name, "signature"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + attempt_count: __sdk::__query_builder::Col::new(table_name, "attempt_count"), + available_at: __sdk::__query_builder::Col::new(table_name, "available_at"), + lease_worker_id: __sdk::__query_builder::Col::new(table_name, "lease_worker_id"), + lease_expires_at: __sdk::__query_builder::Col::new(table_name, "lease_expires_at"), + last_error_message: __sdk::__query_builder::Col::new(table_name, "last_error_message"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `PaymentWebhookDelivery`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PaymentWebhookDeliveryIxCols { + pub delivery_id: __sdk::__query_builder::IxCol, + pub order_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PaymentWebhookDelivery { + type IxCols = PaymentWebhookDeliveryIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PaymentWebhookDeliveryIxCols { + delivery_id: __sdk::__query_builder::IxCol::new(table_name, "delivery_id"), + order_id: __sdk::__query_builder::IxCol::new(table_name, "order_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PaymentWebhookDelivery {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_enqueue_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_enqueue_input_type.rs new file mode 100644 index 000000000..d44d70835 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_enqueue_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookEnqueueInput { + pub order_id: String, + pub payload_json: String, + pub signature: String, + pub now_micros: i64, +} + +impl __sdk::InModule for PaymentWebhookEnqueueInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_procedure_result_type.rs new file mode 100644 index 000000000..0ffbd510e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/payment_webhook_procedure_result_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_webhook_delivery_snapshot_type::PaymentWebhookDeliverySnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PaymentWebhookProcedureResult { + pub ok: bool, + pub delivery: Option, + pub deliveries: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for PaymentWebhookProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/revoke_payment_api_key_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/revoke_payment_api_key_and_return_procedure.rs new file mode 100644 index 000000000..659adfe4c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/revoke_payment_api_key_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_api_key_revoke_input_type::PaymentApiKeyRevokeInput; +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct RevokePaymentApiKeyAndReturnArgs { + pub input: PaymentApiKeyRevokeInput, +} + +impl __sdk::InModule for RevokePaymentApiKeyAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `revoke_payment_api_key_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait revoke_payment_api_key_and_return { + fn revoke_payment_api_key_and_return(&self, input: PaymentApiKeyRevokeInput) { + self.revoke_payment_api_key_and_return_then(input, |_, _| {}); + } + + fn revoke_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyRevokeInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl revoke_payment_api_key_and_return for super::RemoteProcedures { + fn revoke_payment_api_key_and_return_then( + &self, + input: PaymentApiKeyRevokeInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "revoke_payment_api_key_and_return", + RevokePaymentApiKeyAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/set_payment_order_provider_code_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/set_payment_order_provider_code_and_return_procedure.rs new file mode 100644 index 000000000..c2ee27aa6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/set_payment_order_provider_code_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_order_procedure_result_type::PaymentOrderProcedureResult; +use super::payment_order_provider_code_input_type::PaymentOrderProviderCodeInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct SetPaymentOrderProviderCodeAndReturnArgs { + pub input: PaymentOrderProviderCodeInput, +} + +impl __sdk::InModule for SetPaymentOrderProviderCodeAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `set_payment_order_provider_code_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait set_payment_order_provider_code_and_return { + fn set_payment_order_provider_code_and_return(&self, input: PaymentOrderProviderCodeInput) { + self.set_payment_order_provider_code_and_return_then(input, |_, _| {}); + } + + fn set_payment_order_provider_code_and_return_then( + &self, + input: PaymentOrderProviderCodeInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl set_payment_order_provider_code_and_return for super::RemoteProcedures { + fn set_payment_order_provider_code_and_return_then( + &self, + input: PaymentOrderProviderCodeInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentOrderProcedureResult>( + "set_payment_order_provider_code_and_return", + SetPaymentOrderProviderCodeAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_payment_app_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_payment_app_and_return_procedure.rs new file mode 100644 index 000000000..c87253b43 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_payment_app_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::payment_app_procedure_result_type::PaymentAppProcedureResult; +use super::payment_app_update_input_type::PaymentAppUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdatePaymentAppAndReturnArgs { + pub input: PaymentAppUpdateInput, +} + +impl __sdk::InModule for UpdatePaymentAppAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_payment_app_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_payment_app_and_return { + fn update_payment_app_and_return(&self, input: PaymentAppUpdateInput) { + self.update_payment_app_and_return_then(input, |_, _| {}); + } + + fn update_payment_app_and_return_then( + &self, + input: PaymentAppUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_payment_app_and_return for super::RemoteProcedures { + fn update_payment_app_and_return_then( + &self, + input: PaymentAppUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, PaymentAppProcedureResult>( + "update_payment_app_and_return", + UpdatePaymentAppAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/src/active.rs b/server-rs/crates/spacetime-module/src/active.rs index de0a60919..12d2789e2 100644 --- a/server-rs/crates/spacetime-module/src/active.rs +++ b/server-rs/crates/spacetime-module/src/active.rs @@ -29,6 +29,7 @@ mod external_generation; mod game_distribution; #[path = "llm_router_account.rs"] mod llm_router_account_storage; +mod payment_storage; pub use agc_models::*; mod migration; mod runtime; @@ -46,6 +47,7 @@ pub use external_generation::*; pub use game_distribution::*; pub use llm_router_account_storage::*; pub use migration::*; +pub use payment_storage::*; pub use runtime::*; // Host-side unit tests need to link the module crate as a normal test binary. diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index ba1738869..f942967bb 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -204,6 +204,10 @@ macro_rules! migration_tables { asset_entity_binding, asset_event, external_api_key, + payment_app, + payment_api_key, + payment_order, + payment_webhook_delivery, // 游戏分发:游戏身份、不可变发行版本和幂等收据都属于业务事实, // 随迁移导出/导入;私有 ZIP 与展开内容保存在对象存储,不进入这两张表。 // 资料随版本冻结:版本表末尾追加 metadata_json,游戏表末尾追加 diff --git a/server-rs/crates/spacetime-module/src/payment_storage.rs b/server-rs/crates/spacetime-module/src/payment_storage.rs new file mode 100644 index 000000000..375c1c641 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/payment_storage.rs @@ -0,0 +1,1199 @@ +use crate::*; + +#[spacetimedb::table( + accessor = payment_app, + index(accessor = by_payment_app_owner_user_id, btree(columns = [owner_user_id])) +)] +#[derive(Clone)] +pub struct PaymentApp { + #[primary_key] + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub callback_url: Option, + pub enabled: bool, + pub created_at: Timestamp, + pub updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = payment_api_key, + index(accessor = by_payment_api_key_app_id, btree(columns = [app_id])), + index(accessor = by_payment_api_key_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct PaymentApiKey { + #[primary_key] + pub key_id: String, + pub app_id: String, + pub owner_user_id: String, + pub name: String, + pub key_prefix: String, + #[unique] + pub key_hash: String, + pub scopes_json: String, + pub created_at: Timestamp, + pub last_used_at: Option, + pub revoked_at: Option, + pub updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = payment_order, + index(accessor = by_payment_order_app_id, btree(columns = [app_id])), + index(accessor = by_payment_order_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_payment_order_status, btree(columns = [status])) +)] +#[derive(Clone)] +pub struct PaymentOrder { + #[primary_key] + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub merchant_order_id: String, + #[unique] + pub idempotency_lookup: String, + #[unique] + pub merchant_order_lookup: String, + pub title: String, + pub items_json: String, + pub amount_cents: u64, + pub currency: String, + pub provider: PaymentProvider, + pub provider_trade_no: Option, + #[unique] + pub checkout_token: String, + pub status: PaymentOrderStatus, + pub created_at: Timestamp, + pub expires_at: Timestamp, + pub paid_at: Option, + pub updated_at: Timestamp, + pub provider_code_url: Option, + pub callback_url: Option, + pub callback_signing_key_hash: Option, + #[default(None::)] + pub provider_attempted_at: Option, +} + +#[spacetimedb::table( + accessor = payment_webhook_delivery, + index(accessor = by_payment_webhook_delivery_status_available, btree(columns = [status, available_at])), + index(accessor = by_payment_webhook_delivery_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_payment_webhook_delivery_order_id, btree(columns = [order_id])) +)] +#[derive(Clone)] +pub struct PaymentWebhookDelivery { + #[primary_key] + pub delivery_id: String, + pub order_id: String, + pub app_id: String, + pub owner_user_id: String, + pub callback_url: String, + pub payload_json: String, + pub signature: String, + pub status: String, + pub attempt_count: u32, + pub available_at: Timestamp, + pub lease_worker_id: Option, + pub lease_expires_at: Option, + pub last_error_message: Option, + pub created_at: Timestamp, + pub updated_at: Timestamp, +} + +const PAYMENT_WEBHOOK_STATUS_PENDING: &str = "pending"; +const PAYMENT_WEBHOOK_STATUS_PROCESSING: &str = "processing"; +const PAYMENT_WEBHOOK_STATUS_DELIVERED: &str = "delivered"; +const PAYMENT_WEBHOOK_STATUS_FAILED: &str = "failed"; + +#[spacetimedb::procedure] +pub fn create_payment_app_and_return( + ctx: &mut ProcedureContext, + input: PaymentAppCreateInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + create_payment_app(tx, input.clone()) + }) { + Ok(app) => PaymentAppProcedureResult { + ok: true, + app: Some(app), + key: None, + apps: Vec::new(), + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn list_payment_apps_and_return( + ctx: &mut ProcedureContext, + input: PaymentAppListInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + list_payment_apps(tx, input.clone()) + }) { + Ok(apps) => PaymentAppProcedureResult { + ok: true, + app: None, + key: None, + apps, + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn update_payment_app_and_return( + ctx: &mut ProcedureContext, + input: PaymentAppUpdateInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + update_payment_app(tx, input.clone()) + }) { + Ok(app) => PaymentAppProcedureResult { + ok: true, + app: Some(app), + key: None, + apps: Vec::new(), + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn list_payment_api_keys_and_return( + ctx: &mut ProcedureContext, + input: PaymentApiKeyListInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + list_payment_api_keys(tx, input.clone()) + }) { + Ok(keys) => PaymentAppProcedureResult { + ok: true, + app: None, + key: None, + apps: Vec::new(), + keys, + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn create_payment_api_key_and_return( + ctx: &mut ProcedureContext, + input: PaymentApiKeyCreateInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + create_payment_api_key(tx, input.clone()) + }) { + Ok(key) => PaymentAppProcedureResult { + ok: true, + app: None, + key: Some(key), + apps: Vec::new(), + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn revoke_payment_api_key_and_return( + ctx: &mut ProcedureContext, + input: PaymentApiKeyRevokeInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + revoke_payment_api_key(tx, input.clone()) + }) { + Ok(key) => PaymentAppProcedureResult { + ok: true, + app: None, + key: Some(key), + apps: Vec::new(), + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn authenticate_payment_api_key_and_return( + ctx: &mut ProcedureContext, + input: PaymentApiKeyAuthenticateInput, +) -> PaymentAppProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + authenticate_payment_api_key(tx, input.clone()) + }) { + Ok((app, key)) => PaymentAppProcedureResult { + ok: true, + app: Some(app), + key: Some(key), + apps: Vec::new(), + keys: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_app_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn create_payment_order_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderCreateInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + create_payment_order(tx, input.clone()) + }) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn get_payment_order_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderGetInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + get_payment_order(tx, input.clone()) + }) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn get_payment_order_by_checkout_token_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderCheckoutGetInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + get_payment_order_by_checkout_token(tx, input.clone()) + }) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn list_payment_orders_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderListInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + list_payment_orders(tx, input.clone()) + }) { + Ok(orders) => PaymentOrderProcedureResult { + ok: true, + order: None, + orders, + error_message: None, + }, + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn mark_payment_order_paid_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderMarkPaidInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + mark_payment_order_paid(tx, input.clone()) + }) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn set_payment_order_provider_code_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderProviderCodeInput, +) -> PaymentOrderProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + set_payment_order_provider_code(tx, input.clone()) + }) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn mark_payment_order_provider_attempted_and_return( + ctx: &mut ProcedureContext, + input: PaymentOrderProviderAttemptInput, +) -> PaymentOrderProcedureResult { + match ctx.try_with_tx(|tx| mark_payment_order_provider_attempted(tx, input.clone())) { + Ok(order) => payment_order_single_ok(order), + Err(error_message) => payment_order_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn enqueue_payment_webhook_and_return( + ctx: &mut ProcedureContext, + input: PaymentWebhookEnqueueInput, +) -> PaymentWebhookProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + enqueue_payment_webhook(tx, input.clone()) + }) { + Ok(delivery) => PaymentWebhookProcedureResult { + ok: true, + delivery, + deliveries: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_webhook_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn claim_payment_webhooks_and_return( + ctx: &mut ProcedureContext, + input: PaymentWebhookClaimInput, +) -> PaymentWebhookProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + claim_payment_webhooks(tx, input.clone()) + }) { + Ok(deliveries) => PaymentWebhookProcedureResult { + ok: true, + delivery: None, + deliveries, + error_message: None, + }, + Err(error_message) => payment_webhook_error(error_message), + } +} + +#[spacetimedb::procedure] +pub fn complete_payment_webhook_and_return( + ctx: &mut ProcedureContext, + input: PaymentWebhookCompleteInput, +) -> PaymentWebhookProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::require_editor_generation_runtime_service_identity(tx, caller)?; + complete_payment_webhook(tx, input.clone()) + }) { + Ok(delivery) => PaymentWebhookProcedureResult { + ok: true, + delivery: Some(delivery), + deliveries: Vec::new(), + error_message: None, + }, + Err(error_message) => payment_webhook_error(error_message), + } +} + +fn create_payment_app( + ctx: &ReducerContext, + input: PaymentAppCreateInput, +) -> Result { + let input = build_payment_app_create_input( + input.app_id, + input.owner_user_id, + input.name, + input.callback_url, + input.now_micros, + )?; + if ctx.db.payment_app().app_id().find(&input.app_id).is_some() { + return Err("支付应用 ID 已存在".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ctx.db.payment_app().insert(PaymentApp { + app_id: input.app_id.clone(), + owner_user_id: input.owner_user_id, + name: input.name, + callback_url: input.callback_url, + enabled: true, + created_at: now, + updated_at: now, + }); + ctx.db + .payment_app() + .app_id() + .find(&input.app_id) + .map(|row| payment_app_snapshot(&row)) + .ok_or_else(|| "支付应用创建后读取失败".to_string()) +} + +fn list_payment_apps( + ctx: &ReducerContext, + input: PaymentAppListInput, +) -> Result, String> { + let owner_user_id = required(input.owner_user_id, "payment_app.owner_user_id")?; + let mut apps = ctx + .db + .payment_app() + .by_payment_app_owner_user_id() + .filter(&owner_user_id) + .map(|row| payment_app_snapshot(&row)) + .collect::>(); + apps.sort_by(|left, right| right.created_at_micros.cmp(&left.created_at_micros)); + Ok(apps) +} + +fn update_payment_app( + ctx: &ReducerContext, + input: PaymentAppUpdateInput, +) -> Result { + let app_id = required(input.app_id, "payment_app.app_id")?; + let owner_user_id = required(input.owner_user_id, "payment_app.owner_user_id")?; + let row = ctx + .db + .payment_app() + .app_id() + .find(&app_id) + .ok_or_else(|| "支付应用不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问支付应用".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + ctx.db.payment_app().app_id().delete(&app_id); + ctx.db.payment_app().insert(PaymentApp { + enabled: input.enabled, + updated_at: now, + ..row + }); + ctx.db + .payment_app() + .app_id() + .find(&app_id) + .map(|row| payment_app_snapshot(&row)) + .ok_or_else(|| "支付应用更新后读取失败".to_string()) +} + +fn list_payment_api_keys( + ctx: &ReducerContext, + input: PaymentApiKeyListInput, +) -> Result, String> { + let owner_user_id = required(input.owner_user_id, "payment_api_key.owner_user_id")?; + let mut keys = ctx + .db + .payment_api_key() + .by_payment_api_key_owner_user_id() + .filter(&owner_user_id) + .map(|row| payment_api_key_snapshot(&row)) + .collect::>(); + keys.sort_by(|left, right| right.created_at_micros.cmp(&left.created_at_micros)); + Ok(keys) +} + +fn create_payment_api_key( + ctx: &ReducerContext, + input: PaymentApiKeyCreateInput, +) -> Result { + let input = build_payment_api_key_create_input( + input.key_id, + input.app_id, + input.owner_user_id, + input.name, + input.key_prefix, + input.key_hash, + input.scopes_json, + input.now_micros, + )?; + let app = ctx + .db + .payment_app() + .app_id() + .find(&input.app_id) + .ok_or_else(|| "支付应用不存在".to_string())?; + if app.owner_user_id != input.owner_user_id || !app.enabled { + return Err("支付应用不可用".to_string()); + } + if ctx + .db + .payment_api_key() + .key_hash() + .find(&input.key_hash) + .is_some() + { + return Err("支付 API Key 摘要已存在".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ctx.db.payment_api_key().insert(PaymentApiKey { + key_id: input.key_id.clone(), + app_id: input.app_id, + owner_user_id: input.owner_user_id, + name: input.name, + key_prefix: input.key_prefix, + key_hash: input.key_hash, + scopes_json: input.scopes_json, + created_at: now, + last_used_at: None, + revoked_at: None, + updated_at: now, + }); + ctx.db + .payment_api_key() + .key_id() + .find(&input.key_id) + .map(|row| payment_api_key_snapshot(&row)) + .ok_or_else(|| "支付 API Key 创建后读取失败".to_string()) +} + +fn revoke_payment_api_key( + ctx: &ReducerContext, + input: PaymentApiKeyRevokeInput, +) -> Result { + let key_id = required(input.key_id, "payment_api_key.key_id")?; + let owner_user_id = required(input.owner_user_id, "payment_api_key.owner_user_id")?; + let row = ctx + .db + .payment_api_key() + .key_id() + .find(&key_id) + .ok_or_else(|| "支付 API Key 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问支付 API Key".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.revoked_at_micros); + ctx.db.payment_api_key().key_id().delete(&key_id); + ctx.db.payment_api_key().insert(PaymentApiKey { + revoked_at: row.revoked_at.or(Some(now)), + updated_at: now, + ..row + }); + ctx.db + .payment_api_key() + .key_id() + .find(&key_id) + .map(|row| payment_api_key_snapshot(&row)) + .ok_or_else(|| "支付 API Key 撤销后读取失败".to_string()) +} + +fn authenticate_payment_api_key( + ctx: &ReducerContext, + input: PaymentApiKeyAuthenticateInput, +) -> Result<(PaymentAppSnapshot, PaymentApiKeySnapshot), String> { + let key_hash = required(input.key_hash, "payment_api_key.key_hash")?; + let key = ctx + .db + .payment_api_key() + .key_hash() + .find(&key_hash) + .ok_or_else(|| "支付 API Key 不存在或已失效".to_string())?; + if key.revoked_at.is_some() { + return Err("支付 API Key 不存在或已失效".to_string()); + } + let app = ctx + .db + .payment_app() + .app_id() + .find(&key.app_id) + .ok_or_else(|| "支付应用不存在".to_string())?; + if !app.enabled || app.owner_user_id != key.owner_user_id { + return Err("支付应用不可用".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.used_at_micros); + let key_id = key.key_id.clone(); + ctx.db.payment_api_key().key_id().delete(&key_id); + ctx.db.payment_api_key().insert(PaymentApiKey { + last_used_at: Some(now), + updated_at: now, + ..key + }); + let key = ctx + .db + .payment_api_key() + .key_id() + .find(&key_id) + .ok_or_else(|| "支付 API Key 更新失败".to_string())?; + Ok((payment_app_snapshot(&app), payment_api_key_snapshot(&key))) +} + +fn create_payment_order( + ctx: &ReducerContext, + input: PaymentOrderCreateInput, +) -> Result { + let input = build_payment_order_create_input( + input.order_id, + input.app_id, + input.owner_user_id, + input.merchant_order_id, + input.idempotency_lookup, + input.title, + input.items_json, + input.amount_cents, + input.currency, + input.provider, + input.checkout_token, + input.now_micros, + input.expires_at_micros, + input.callback_url.clone(), + input.callback_signing_key_hash.clone(), + )?; + let app = ctx + .db + .payment_app() + .app_id() + .find(&input.app_id) + .ok_or_else(|| "支付应用不存在".to_string())?; + if !app.enabled || app.owner_user_id != input.owner_user_id { + return Err("支付应用不可用".to_string()); + } + if let Some(existing) = ctx + .db + .payment_order() + .idempotency_lookup() + .find(&input.idempotency_lookup) + { + if existing.app_id != input.app_id + || existing.amount_cents != input.amount_cents + || existing.title != input.title + || existing.items_json != input.items_json + { + return Err("相同幂等键对应的订单字段不一致".to_string()); + } + return Ok(payment_order_snapshot(&existing)); + } + let merchant_order_lookup = format!("{}:{}", input.app_id, input.merchant_order_id); + if ctx + .db + .payment_order() + .merchant_order_lookup() + .find(&merchant_order_lookup) + .is_some() + { + return Err("接入方订单号已存在".to_string()); + } + if ctx + .db + .payment_order() + .checkout_token() + .find(&input.checkout_token) + .is_some() + { + return Err("收银台 token 已存在".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + let expires_at = Timestamp::from_micros_since_unix_epoch(input.expires_at_micros); + ctx.db.payment_order().insert(PaymentOrder { + order_id: input.order_id.clone(), + app_id: input.app_id, + owner_user_id: input.owner_user_id, + merchant_order_id: input.merchant_order_id, + idempotency_lookup: input.idempotency_lookup, + merchant_order_lookup, + title: input.title, + items_json: input.items_json, + amount_cents: input.amount_cents, + currency: input.currency, + provider: input.provider, + provider_trade_no: None, + checkout_token: input.checkout_token, + status: PaymentOrderStatus::Pending, + created_at: now, + expires_at, + paid_at: None, + updated_at: now, + provider_code_url: None, + callback_url: input.callback_url, + callback_signing_key_hash: input.callback_signing_key_hash, + provider_attempted_at: None, + }); + ctx.db + .payment_order() + .order_id() + .find(&input.order_id) + .map(|row| payment_order_snapshot(&row)) + .ok_or_else(|| "支付订单创建后读取失败".to_string()) +} + +fn get_payment_order( + ctx: &ReducerContext, + input: PaymentOrderGetInput, +) -> Result { + let order_id = required(input.order_id, "payment_order.order_id")?; + let order = ctx + .db + .payment_order() + .order_id() + .find(&order_id) + .ok_or_else(|| "支付订单不存在".to_string())?; + if let Some(owner_user_id) = input.owner_user_id { + if order.owner_user_id != required(owner_user_id, "payment_order.owner_user_id")? { + return Err("无权访问支付订单".to_string()); + } + } + Ok(payment_order_snapshot(&order)) +} + +fn get_payment_order_by_checkout_token( + ctx: &ReducerContext, + input: PaymentOrderCheckoutGetInput, +) -> Result { + let token = required(input.checkout_token, "payment_order.checkout_token")?; + ctx.db + .payment_order() + .checkout_token() + .find(&token) + .map(|order| payment_order_snapshot(&order)) + .ok_or_else(|| "支付订单不存在".to_string()) +} + +fn list_payment_orders( + ctx: &ReducerContext, + input: PaymentOrderListInput, +) -> Result, String> { + let owner_user_id = required(input.owner_user_id, "payment_order.owner_user_id")?; + let limit = input.limit.clamp(1, 100) as usize; + let mut orders = match input.app_id { + Some(app_id) => ctx + .db + .payment_order() + .by_payment_order_app_id() + .filter(&required(app_id, "payment_order.app_id")?) + .filter(|order| order.owner_user_id == owner_user_id) + .map(|row| payment_order_snapshot(&row)) + .collect::>(), + None => ctx + .db + .payment_order() + .by_payment_order_owner_user_id() + .filter(&owner_user_id) + .map(|row| payment_order_snapshot(&row)) + .collect::>(), + }; + orders.sort_by(|left, right| right.created_at_micros.cmp(&left.created_at_micros)); + orders.truncate(limit); + Ok(orders) +} + +fn mark_payment_order_paid( + ctx: &ReducerContext, + input: PaymentOrderMarkPaidInput, +) -> Result { + let input = build_payment_order_mark_paid_input( + input.order_id, + input.provider_trade_no, + input.amount_cents, + input.paid_at_micros, + )?; + let order = ctx + .db + .payment_order() + .order_id() + .find(&input.order_id) + .ok_or_else(|| "支付订单不存在".to_string())?; + if order.amount_cents != input.amount_cents { + return Err("支付金额与本地订单不一致".to_string()); + } + if order.status == PaymentOrderStatus::Paid { + if order.provider_trade_no.as_deref() != Some(input.provider_trade_no.as_str()) { + return Err("已支付订单的平台交易号不一致".to_string()); + } + return Ok(payment_order_snapshot(&order)); + } + if matches!( + order.status, + PaymentOrderStatus::Closed | PaymentOrderStatus::Refunded + ) { + return Err("当前支付订单状态不能确认支付".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.paid_at_micros); + ctx.db.payment_order().order_id().delete(&order.order_id); + ctx.db.payment_order().insert(PaymentOrder { + status: PaymentOrderStatus::Paid, + provider_trade_no: Some(input.provider_trade_no), + paid_at: Some(now), + updated_at: now, + ..order + }); + ctx.db + .payment_order() + .order_id() + .find(&input.order_id) + .map(|row| payment_order_snapshot(&row)) + .ok_or_else(|| "支付订单确认后读取失败".to_string()) +} + +fn set_payment_order_provider_code( + ctx: &ReducerContext, + input: PaymentOrderProviderCodeInput, +) -> Result { + let order_id = required(input.order_id, "payment_order.order_id")?; + let provider_code_url = required(input.provider_code_url, "payment_order.provider_code_url")?; + let order = ctx + .db + .payment_order() + .order_id() + .find(&order_id) + .ok_or_else(|| "支付订单不存在".to_string())?; + if matches!( + order.status, + PaymentOrderStatus::Paid | PaymentOrderStatus::Closed + ) { + return Ok(payment_order_snapshot(&order)); + } + let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + ctx.db.payment_order().order_id().delete(&order_id); + ctx.db.payment_order().insert(PaymentOrder { + provider_code_url: Some(provider_code_url), + updated_at, + ..order + }); + ctx.db + .payment_order() + .order_id() + .find(&order_id) + .map(|row| payment_order_snapshot(&row)) + .ok_or_else(|| "支付订单 provider 结果写入后读取失败".to_string()) +} + +fn mark_payment_order_provider_attempted( + ctx: &ReducerContext, + input: PaymentOrderProviderAttemptInput, +) -> Result { + let order_id = required(input.order_id, "payment_order.order_id")?; + let order = ctx + .db + .payment_order() + .order_id() + .find(&order_id) + .ok_or_else(|| "支付订单不存在".to_string())?; + if order.provider_code_url.is_some() || order.status == PaymentOrderStatus::Paid { + return Ok(payment_order_snapshot(&order)); + } + if order.provider_attempted_at.is_some() { + return Err("PAYMENT_PROVIDER_PENDING".to_string()); + } + let attempted_at = Timestamp::from_micros_since_unix_epoch(input.attempted_at_micros); + ctx.db.payment_order().order_id().delete(&order_id); + ctx.db.payment_order().insert(PaymentOrder { + status: PaymentOrderStatus::Paying, + provider_attempted_at: Some(attempted_at), + updated_at: attempted_at, + ..order + }); + ctx.db + .payment_order() + .order_id() + .find(&order_id) + .map(|row| payment_order_snapshot(&row)) + .ok_or_else(|| "支付订单 provider claim 后读取失败".to_string()) +} + +fn enqueue_payment_webhook( + ctx: &ReducerContext, + input: PaymentWebhookEnqueueInput, +) -> Result, String> { + let order_id = required(input.order_id, "payment_webhook.order_id")?; + let payload_json = required(input.payload_json, "payment_webhook.payload_json")?; + let signature = required(input.signature, "payment_webhook.signature")?; + if payload_json.len() > 64 * 1024 { + return Err("payment_webhook.payload_json 超出大小限制".to_string()); + } + let order = ctx + .db + .payment_order() + .order_id() + .find(&order_id) + .ok_or_else(|| "支付订单不存在".to_string())?; + if order.status != PaymentOrderStatus::Paid { + return Err("只有已支付订单可以创建外部回调".to_string()); + } + let Some(callback_url) = order.callback_url.clone() else { + return Ok(None); + }; + let delivery_id = format!("payment-webhook:{order_id}"); + if let Some(existing) = ctx + .db + .payment_webhook_delivery() + .delivery_id() + .find(&delivery_id) + { + if existing.payload_json != payload_json || existing.signature != signature { + return Err("相同支付订单的回调内容不一致".to_string()); + } + return Ok(Some(payment_webhook_snapshot(&existing))); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ctx.db + .payment_webhook_delivery() + .insert(PaymentWebhookDelivery { + delivery_id: delivery_id.clone(), + order_id: order.order_id, + app_id: order.app_id, + owner_user_id: order.owner_user_id, + callback_url, + payload_json, + signature, + status: PAYMENT_WEBHOOK_STATUS_PENDING.to_string(), + attempt_count: 0, + available_at: now, + lease_worker_id: None, + lease_expires_at: None, + last_error_message: None, + created_at: now, + updated_at: now, + }); + ctx.db + .payment_webhook_delivery() + .delivery_id() + .find(&delivery_id) + .map(|row| payment_webhook_snapshot(&row)) + .map(Some) + .ok_or_else(|| "支付回调写入后读取失败".to_string()) +} + +fn claim_payment_webhooks( + ctx: &ReducerContext, + input: PaymentWebhookClaimInput, +) -> Result, String> { + let worker_id = required(input.worker_id, "payment_webhook.worker_id")?; + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + let lease_expires_at = Timestamp::from_micros_since_unix_epoch(input.lease_expires_at_micros); + let limit = input.limit.clamp(1, 50) as usize; + let mut rows = ctx + .db + .payment_webhook_delivery() + .iter() + .filter(|row| { + (row.status == PAYMENT_WEBHOOK_STATUS_PENDING && row.available_at <= now) + || (row.status == PAYMENT_WEBHOOK_STATUS_PROCESSING + && row.lease_expires_at.is_some_and(|value| value <= now)) + }) + .collect::>(); + rows.sort_by(|left, right| left.available_at.cmp(&right.available_at)); + rows.truncate(limit); + let mut claimed = Vec::with_capacity(rows.len()); + for row in rows { + let delivery_id = row.delivery_id.clone(); + ctx.db + .payment_webhook_delivery() + .delivery_id() + .delete(&delivery_id); + ctx.db + .payment_webhook_delivery() + .insert(PaymentWebhookDelivery { + status: PAYMENT_WEBHOOK_STATUS_PROCESSING.to_string(), + lease_worker_id: Some(worker_id.clone()), + lease_expires_at: Some(lease_expires_at), + updated_at: now, + ..row + }); + if let Some(updated) = ctx + .db + .payment_webhook_delivery() + .delivery_id() + .find(&delivery_id) + { + claimed.push(payment_webhook_snapshot(&updated)); + } + } + Ok(claimed) +} + +fn complete_payment_webhook( + ctx: &ReducerContext, + input: PaymentWebhookCompleteInput, +) -> Result { + let delivery_id = required(input.delivery_id, "payment_webhook.delivery_id")?; + let worker_id = required(input.worker_id, "payment_webhook.worker_id")?; + let mut row = ctx + .db + .payment_webhook_delivery() + .delivery_id() + .find(&delivery_id) + .ok_or_else(|| "支付回调投递不存在".to_string())?; + if row.status != PAYMENT_WEBHOOK_STATUS_PROCESSING + || row.lease_worker_id.as_deref() != Some(worker_id.as_str()) + { + return Err("支付回调投递租约不属于当前 worker".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + row.lease_worker_id = None; + row.lease_expires_at = None; + row.updated_at = now; + if input.success { + row.status = PAYMENT_WEBHOOK_STATUS_DELIVERED.to_string(); + row.last_error_message = None; + } else { + row.attempt_count = row.attempt_count.saturating_add(1); + row.last_error_message = input + .error_message + .and_then(|value| normalize_optional_text(Some(value))); + if row.attempt_count >= PAYMENT_WEBHOOK_MAX_ATTEMPTS { + row.status = PAYMENT_WEBHOOK_STATUS_FAILED.to_string(); + } else { + row.status = PAYMENT_WEBHOOK_STATUS_PENDING.to_string(); + row.available_at = + Timestamp::from_micros_since_unix_epoch(input.next_available_at_micros); + } + } + ctx.db + .payment_webhook_delivery() + .delivery_id() + .delete(&delivery_id); + ctx.db.payment_webhook_delivery().insert(row.clone()); + Ok(payment_webhook_snapshot(&row)) +} + +fn payment_app_snapshot(row: &PaymentApp) -> PaymentAppSnapshot { + PaymentAppSnapshot { + app_id: row.app_id.clone(), + owner_user_id: row.owner_user_id.clone(), + name: row.name.clone(), + callback_url: row.callback_url.clone(), + enabled: row.enabled, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn payment_api_key_snapshot(row: &PaymentApiKey) -> PaymentApiKeySnapshot { + PaymentApiKeySnapshot { + key_id: row.key_id.clone(), + app_id: row.app_id.clone(), + owner_user_id: row.owner_user_id.clone(), + name: row.name.clone(), + key_prefix: row.key_prefix.clone(), + scopes_json: row.scopes_json.clone(), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + last_used_at_micros: row + .last_used_at + .map(|value| value.to_micros_since_unix_epoch()), + revoked_at_micros: row + .revoked_at + .map(|value| value.to_micros_since_unix_epoch()), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn payment_order_snapshot(row: &PaymentOrder) -> PaymentOrderSnapshot { + PaymentOrderSnapshot { + order_id: row.order_id.clone(), + app_id: row.app_id.clone(), + owner_user_id: row.owner_user_id.clone(), + merchant_order_id: row.merchant_order_id.clone(), + idempotency_lookup: row.idempotency_lookup.clone(), + title: row.title.clone(), + items_json: row.items_json.clone(), + amount_cents: row.amount_cents, + currency: row.currency.clone(), + provider: row.provider, + provider_trade_no: row.provider_trade_no.clone(), + provider_code_url: row.provider_code_url.clone(), + callback_url: row.callback_url.clone(), + callback_signing_key_hash: row.callback_signing_key_hash.clone(), + provider_attempted_at_micros: row + .provider_attempted_at + .map(|value| value.to_micros_since_unix_epoch()), + checkout_token: row.checkout_token.clone(), + status: row.status, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + expires_at_micros: row.expires_at.to_micros_since_unix_epoch(), + paid_at_micros: row.paid_at.map(|value| value.to_micros_since_unix_epoch()), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn payment_webhook_snapshot(row: &PaymentWebhookDelivery) -> PaymentWebhookDeliverySnapshot { + PaymentWebhookDeliverySnapshot { + delivery_id: row.delivery_id.clone(), + order_id: row.order_id.clone(), + app_id: row.app_id.clone(), + owner_user_id: row.owner_user_id.clone(), + callback_url: row.callback_url.clone(), + payload_json: row.payload_json.clone(), + signature: row.signature.clone(), + status: row.status.clone(), + attempt_count: row.attempt_count, + available_at_micros: row.available_at.to_micros_since_unix_epoch(), + lease_worker_id: row.lease_worker_id.clone(), + lease_expires_at_micros: row + .lease_expires_at + .map(|value| value.to_micros_since_unix_epoch()), + last_error_message: row.last_error_message.clone(), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn payment_app_error(error_message: String) -> PaymentAppProcedureResult { + PaymentAppProcedureResult { + ok: false, + app: None, + key: None, + apps: Vec::new(), + keys: Vec::new(), + error_message: Some(error_message), + } +} + +fn payment_order_single_ok(order: PaymentOrderSnapshot) -> PaymentOrderProcedureResult { + PaymentOrderProcedureResult { + ok: true, + order: Some(order), + orders: Vec::new(), + error_message: None, + } +} + +fn payment_order_error(error_message: String) -> PaymentOrderProcedureResult { + PaymentOrderProcedureResult { + ok: false, + order: None, + orders: Vec::new(), + error_message: Some(error_message), + } +} + +fn payment_webhook_error(error_message: String) -> PaymentWebhookProcedureResult { + PaymentWebhookProcedureResult { + ok: false, + delivery: None, + deliveries: Vec::new(), + error_message: Some(error_message), + } +} + +fn required(value: String, field: &str) -> Result { + let value = value.trim().to_string(); + if value.is_empty() { + return Err(format!("{field} 不能为空")); + } + Ok(value) +} diff --git a/src/components/platform-entry/PaymentCheckoutView.tsx b/src/components/platform-entry/PaymentCheckoutView.tsx new file mode 100644 index 000000000..f1bf8a149 --- /dev/null +++ b/src/components/platform-entry/PaymentCheckoutView.tsx @@ -0,0 +1,199 @@ +import { + CheckCircle2, + Clock3, + Copy, + LoaderCircle, + ShieldCheck, +} from 'lucide-react'; +import QRCode from 'qrcode'; +import { useEffect, useState } from 'react'; + +import type { PaymentOrder } from '../../../packages/shared/src/contracts/payment'; +import { getPublicPaymentCheckout } from '../../services/payment/paymentCheckoutClient'; + +type PaymentCheckoutViewProps = { + checkoutToken: string; +}; + +export function PaymentCheckoutView({ + checkoutToken, +}: PaymentCheckoutViewProps) { + const [order, setOrder] = useState(null); + const [qrDataUrl, setQrDataUrl] = useState(''); + const [error, setError] = useState(''); + + useEffect(() => { + let cancelled = false; + const load = async () => { + try { + const response = await getPublicPaymentCheckout(checkoutToken); + if (!cancelled) { + setOrder(response.order); + setError(''); + } + } catch (nextError: unknown) { + if (!cancelled) { + setError( + nextError instanceof Error ? nextError.message : '读取支付订单失败', + ); + } + } + }; + void load(); + const timer = window.setInterval(() => { + if ( + order?.status === 'paid' || + order?.status === 'closed' || + order?.status === 'expired' + ) { + return; + } + void load(); + }, 5000); + return () => { + cancelled = true; + window.clearInterval(timer); + }; + }, [checkoutToken, order?.status]); + + useEffect(() => { + let cancelled = false; + if (!order?.providerQrCode) { + setQrDataUrl(''); + return; + } + QRCode.toDataURL(order.providerQrCode, { + width: 320, + margin: 2, + color: { dark: '#2c2723', light: '#ffffff' }, + }).then((dataUrl) => { + if (!cancelled) { + setQrDataUrl(dataUrl); + } + }); + return () => { + cancelled = true; + }; + }, [order?.providerQrCode]); + + const copyAmount = () => { + if (order) { + void navigator.clipboard?.writeText((order.amountCents / 100).toFixed(2)); + } + }; + + if (error) { + return ( +
+
{error}
+
+ ); + } + + if (!order) { + return ( +
+
+ + 正在读取支付订单... +
+
+ ); + } + + const amount = (order.amountCents / 100).toFixed(2); + const isPaid = order.status === 'paid'; + const isClosed = order.status === 'closed' || order.status === 'expired'; + + return ( +
+
+
+
+ ¥ +
+ 扫码付款 + 支付成功后自动更新订单状态 +
+
+
+ 订单号 {order.merchantOrderId} +
+

{order.title}

+

+ 请核对商品与订单信息,使用右侧二维码完成支付。 +

+
+ 付款金额说明 + + 请完全按照页面显示的金额付款,少付或多付都可能导致支付无法自动确认。 + +
+
+ {isPaid ? ( + + ) : ( + + )} +
+ + {isPaid ? '支付成功' : isClosed ? '订单已关闭' : '等待支付'} + + + {isPaid + ? '平台已确认到账' + : isClosed + ? '请重新创建订单' + : '完成付款后页面会自动更新状态'} + +
+
+ +
+
+
+ 扫码付款 + {isPaid ? '已完成' : isClosed ? '已关闭' : '支付中'} +
+
+ 应付金额 + ¥{amount} +
+ {isPaid ? ( +
+ + 支付成功 +
+ ) : isClosed ? ( +
+ + 订单已关闭 +
+ ) : qrDataUrl ? ( + 支付二维码 + ) : ( +
+ +
+ )} +
+ + 微信支付 +
+

请使用微信扫一扫完成支付

+
+
+
+ ); +} diff --git a/src/components/platform-entry/PlatformActiveProfileView.tsx b/src/components/platform-entry/PlatformActiveProfileView.tsx index 8b431a2aa..c271c9365 100644 --- a/src/components/platform-entry/PlatformActiveProfileView.tsx +++ b/src/components/platform-entry/PlatformActiveProfileView.tsx @@ -1,6 +1,7 @@ import { Camera, Coins, + CreditCard, History, KeyRound, MessageCircle, @@ -69,6 +70,7 @@ type PlatformActiveProfileViewProps = { mudPointBalance: ProfileMudPointBalance | null; onLogin: () => void; onOpenApiKeys: () => void; + onOpenPaymentService?: () => void; onOpenCommunity: () => void; onOpenFeedback: () => void; onOpenInvite: () => void; @@ -279,6 +281,7 @@ export function PlatformActiveProfileView({ mudPointBalance, onLogin, onOpenApiKeys, + onOpenPaymentService, onOpenCommunity, onOpenFeedback, onOpenInvite, @@ -663,6 +666,11 @@ export function PlatformActiveProfileView({ icon={KeyRound} onClick={onOpenApiKeys} /> + onOpenPaymentService?.()} + /> diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index 26007e677..a6a621256 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -35,6 +35,7 @@ import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { ClientDownloadEntry } from '../creation-home/ClientDownloadEntry'; +import { PaymentCheckoutView } from './PaymentCheckoutView'; import { PlatformActiveMobileWelcomeDialog, shouldOpenActiveMobileWelcomeDialog, @@ -46,6 +47,7 @@ import { import { PlatformActiveProfileView } from './PlatformActiveProfileView'; import type { PlatformEntryFlowShellProps } from './platformEntryActiveTypes'; import { usePlatformDesktopLayout } from './platformEntryResponsive'; +import { PlatformPaymentServiceView } from './PlatformPaymentServiceView'; import { PlatformProfileApiKeysModal } from './PlatformProfileApiKeysModal'; import { canPasteFromNativeHostClipboard, @@ -365,6 +367,7 @@ export function PlatformEntryFlowShellImpl({ const isProfileStage = selectionStage === 'profile' || + selectionStage === 'profile-payment' || (!isDesktopLayout && selectionStage === 'platform'); const profileCenter = usePlatformProfileCenterController({ @@ -403,6 +406,10 @@ export function PlatformEntryFlowShellImpl({ setSelectionStage('profile', { path: '/profile' }); }, [setSelectionStage]); + const openPaymentService = useCallback(() => { + setSelectionStage('profile-payment', { path: '/profile/payment' }); + }, [setSelectionStage]); + const openGames = useCallback(() => { setSelectionStage('games', { path: '/games' }); }, [setSelectionStage]); @@ -575,6 +582,14 @@ export function PlatformEntryFlowShellImpl({ ); } + if (selectionStage === 'payment-checkout') { + const checkoutToken = + typeof window === 'undefined' + ? '' + : (window.location.pathname.split('/').filter(Boolean).at(-1) ?? ''); + return ; + } + const isAuthenticated = Boolean(authUi?.user); const balance = mudPointBalance?.totalPoints ?? legacyWalletBalance; const isCreationStage = @@ -770,7 +785,13 @@ export function PlatformEntryFlowShellImpl({ id={isCreationStage ? 'platform-tab-panel-create' : undefined} className={`platform-tab-panel min-h-0 min-w-0 flex-1 ${isGamePlayStage ? 'platform-tab-panel--game-play' : 'overflow-auto'} ${isCreationStage || isProfileStage || isGamePlayStage ? '' : 'px-3 py-4 sm:px-6 sm:py-6'}`} > - {isProfileStage ? ( + {selectionStage === 'profile-payment' ? ( + authUi?.openLoginModal()} + /> + ) : isProfileStage ? ( authUi?.openLoginModal()} onOpenApiKeys={() => setIsApiKeysOpen(true)} + onOpenPaymentService={openPaymentService} onOpenCommunity={() => profileCenter.openProfilePopupPanel('community') } diff --git a/src/components/platform-entry/PlatformPaymentServiceView.tsx b/src/components/platform-entry/PlatformPaymentServiceView.tsx new file mode 100644 index 000000000..9e91b88ac --- /dev/null +++ b/src/components/platform-entry/PlatformPaymentServiceView.tsx @@ -0,0 +1,347 @@ +import { + ArrowLeft, + Check, + Copy, + CreditCard, + ExternalLink, + KeyRound, +} from 'lucide-react'; +import { useEffect, useState } from 'react'; + +import type { AuthUser } from '@/packages/shared/src'; + +import type { + PaymentApiKey, + PaymentApp, + PaymentAppCreateResponse, +} from '../../../packages/shared/src/contracts/payment'; +import { + createPlatformProfilePaymentApp, + listPlatformProfilePaymentApps, + revokePlatformProfilePaymentApiKey, + updatePlatformProfilePaymentApp, +} from '../../services/platform-entry/platformProfileClient'; +import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; +import { PlatformSubpanel } from '../common/PlatformSubpanel'; + +type PlatformPaymentServiceViewProps = { + user: AuthUser | null | undefined; + onBack: () => void; + onLogin: () => void; +}; + +export function PlatformPaymentServiceView({ + user, + onBack, + onLogin, +}: PlatformPaymentServiceViewProps) { + const [apps, setApps] = useState([]); + const [keys, setKeys] = useState([]); + const [name, setName] = useState('我的游戏支付'); + const [callbackUrl, setCallbackUrl] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [error, setError] = useState(''); + const [created, setCreated] = useState(null); + + useEffect(() => { + if (!user) { + return; + } + let cancelled = false; + setIsLoading(true); + listPlatformProfilePaymentApps() + .then((response) => { + if (!cancelled) { + setApps(response.apps ?? []); + setKeys(response.keys ?? []); + } + }) + .catch((nextError: unknown) => { + if (!cancelled) { + setError( + nextError instanceof Error ? nextError.message : '读取支付服务失败', + ); + } + }) + .finally(() => { + if (!cancelled) { + setIsLoading(false); + } + }); + return () => { + cancelled = true; + }; + }, [user]); + + const submit = async () => { + const normalizedName = name.trim(); + if (!normalizedName) { + setError('请填写应用名称'); + return; + } + setIsCreating(true); + setError(''); + try { + const response = await createPlatformProfilePaymentApp( + normalizedName, + callbackUrl.trim() || null, + ); + setCreated(response); + setApps((current) => [response.app, ...current]); + } catch (nextError: unknown) { + setError( + nextError instanceof Error ? nextError.message : '创建支付应用失败', + ); + } finally { + setIsCreating(false); + } + }; + + const toggleApp = async (app: PaymentApp) => { + try { + const updated = await updatePlatformProfilePaymentApp( + app.appId, + !app.enabled, + ); + setApps((current) => + current.map((item) => (item.appId === updated.appId ? updated : item)), + ); + } catch (nextError: unknown) { + setError( + nextError instanceof Error ? nextError.message : '更新支付应用失败', + ); + } + }; + + const revokeKey = async (key: PaymentApiKey) => { + try { + const updated = await revokePlatformProfilePaymentApiKey(key.keyId); + setKeys((current) => + current.map((item) => (item.keyId === updated.keyId ? updated : item)), + ); + } catch (nextError: unknown) { + setError( + nextError instanceof Error + ? nextError.message + : '撤销支付 API Key 失败', + ); + } + }; + + const copyKey = async () => { + if (!created?.apiKey) { + return; + } + await navigator.clipboard?.writeText(created.apiKey); + }; + + if (!user) { + return ( +
+ + +

支付服务

+

+ 登录后可以创建游戏或外部产品的支付应用。 +

+ + 登录 + +
+
+ ); + } + + return ( +
+
+ +
+

+ 支付服务 +

+

+ 统一收款、动态二维码和支付结果通知。 +

+
+
+ + {error ? ( + + {error} + + ) : null} + + +
+ + +
+

创建接入应用

+

+ 为一款游戏或外部产品创建独立的应用身份。 +

+
+
+
+ + +
+ void submit()} + disabled={isCreating} + > + +
+ + {created ? ( + +
+ + 应用已创建 +
+

+ 完整 API Key 只显示这一次,请立即复制并保存到你的服务端。 +

+
+ + {created.apiKey} + + +
+
+ + {created.key.scopes.join('、')} +
+
+ ) : null} + + +
+

我的支付应用

+ + {isLoading ? '读取中...' : `${apps.length} 个应用`} + +
+
+ {apps.length === 0 && !isLoading ? ( +

+ 还没有支付应用。 +

+ ) : null} + {apps.map((app) => ( +
+
+ {app.name} + + {app.appId} + +
+
+ + {app.enabled ? '可用' : '已停用'} + +
+
+ ))} +
+
+ + +
+

支付 API Key

+ + {keys.length} 个 + +
+
+ {keys.length === 0 ? ( +

+ 创建应用后会生成默认 Key。 +

+ ) : null} + {keys.map((key) => ( +
+
+ {key.name} + + {key.keyPrefix} + +
+ +
+ ))} +
+
+ +

+ + 支付金额由服务端订单快照决定,到账以微信支付通知或查单为准。 +

+
+ ); +} diff --git a/src/components/platform-entry/platformEntryActiveTypes.ts b/src/components/platform-entry/platformEntryActiveTypes.ts index 862fbe957..46af48ad2 100644 --- a/src/components/platform-entry/platformEntryActiveTypes.ts +++ b/src/components/platform-entry/platformEntryActiveTypes.ts @@ -3,6 +3,8 @@ export type SelectionStage = | 'creation-home' | 'project' | 'profile' + | 'profile-payment' + | 'payment-checkout' | 'image-editor' | 'games' | 'game-detail' diff --git a/src/index.css b/src/index.css index 561c0d1bb..71eaa9a09 100644 --- a/src/index.css +++ b/src/index.css @@ -15,6 +15,71 @@ font-display: swap; } +.payment-checkout-page { + min-height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + background: radial-gradient(circle at 18% 12%, rgba(199, 101, 50, 0.11), transparent 32%), #f5f1ea; + color: #2c2723; +} + +.payment-checkout-shell { + width: min(68rem, 100%); + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(20rem, 1.15fr); + gap: 1.25rem; +} + +.payment-checkout-summary, +.payment-checkout-qr-panel { + border: 1px solid rgba(119, 92, 68, 0.13); + border-radius: 1.8rem; + background: rgba(255, 253, 249, 0.92); + box-shadow: 0 1.5rem 3.5rem rgba(92, 70, 49, 0.1); +} + +.payment-checkout-summary { padding: 2rem; } +.payment-checkout-qr-panel { padding: 2rem; text-align: center; } +.payment-checkout-brand { display: flex; align-items: center; gap: 0.75rem; } +.payment-checkout-brand > span { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; border-radius: 0.9rem; background: #f0e4d8; font-weight: 900; } +.payment-checkout-brand strong, .payment-checkout-brand small { display: block; } +.payment-checkout-brand small { margin-top: 0.2rem; color: #88796d; font-size: 0.76rem; } +.payment-checkout-code { margin-top: 2rem; color: #8c7d70; font-size: 0.78rem; } +.payment-checkout-code strong { display: block; margin-top: 0.35rem; color: #2c2723; font-size: 0.9rem; } +.payment-checkout-summary h1 { margin-top: 1.8rem; font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; } +.payment-checkout-description { margin-top: 0.65rem; color: #75675c; font-size: 0.9rem; line-height: 1.7; } +.payment-checkout-alert { display: grid; gap: 0.45rem; margin-top: 1.7rem; padding: 1rem; border: 1px solid #f1d9d1; border-radius: 1rem; background: #fff7f4; color: #8f3e32; font-size: 0.82rem; line-height: 1.6; } +.payment-checkout-state { display: flex; align-items: center; gap: 0.75rem; margin-top: 1.1rem; padding: 1rem; border: 1px solid rgba(119, 92, 68, 0.1); border-radius: 1rem; } +.payment-checkout-state strong, .payment-checkout-state span { display: block; } +.payment-checkout-state span { margin-top: 0.2rem; color: #88796d; font-size: 0.78rem; } +.payment-checkout-copy { display: inline-flex; align-items: center; gap: 0.4rem; margin-top: 1rem; color: #7a5b42; font-size: 0.8rem; font-weight: 700; } +.payment-checkout-qr-title { display: flex; justify-content: space-between; font-size: 0.95rem; } +.payment-checkout-qr-title span { padding: 0.35rem 0.7rem; border: 1px solid rgba(119, 92, 68, 0.12); border-radius: 999px; color: #88796d; font-size: 0.75rem; } +.payment-checkout-amount { display: flex; align-items: center; justify-content: space-between; margin-top: 1.2rem; padding: 1.15rem; border: 1px solid rgba(119, 92, 68, 0.11); border-radius: 1rem; background: #f8f1e9; text-align: left; } +.payment-checkout-amount small { color: #88796d; } +.payment-checkout-amount strong { font-size: 2.1rem; } +.payment-checkout-qr, .payment-checkout-qr-placeholder { width: min(19rem, 100%); aspect-ratio: 1; margin: 1.8rem auto 1rem; border-radius: 1.2rem; background: #fff; } +.payment-checkout-qr { display: block; padding: 0.65rem; border: 1px solid rgba(119, 92, 68, 0.12); } +.payment-checkout-qr-placeholder { display: grid; place-items: center; color: #a17c5b; } +.payment-checkout-success { display: grid; place-items: center; gap: 0.7rem; min-height: 19rem; margin-top: 1.8rem; color: #27845d; } +.payment-checkout-success--muted { color: #998b7f; } +.payment-checkout-provider { display: inline-flex; align-items: center; gap: 0.35rem; font-weight: 800; } +.payment-checkout-hint { margin-top: 0.45rem; color: #88796d; font-size: 0.8rem; } +.payment-checkout-loading, .payment-checkout-error { display: flex; align-items: center; gap: 0.6rem; padding: 1rem 1.25rem; border-radius: 1rem; background: rgba(255, 253, 249, 0.95); box-shadow: 0 1rem 2rem rgba(92, 70, 49, 0.1); } +.payment-checkout-error { color: #8f3e32; } + +@media (max-width: 760px) { + .payment-checkout-page { align-items: flex-start; padding: 1rem; } + .payment-checkout-shell { grid-template-columns: 1fr; gap: 0.8rem; } + .payment-checkout-summary, .payment-checkout-qr-panel { border-radius: 1.25rem; padding: 1.25rem; } + .payment-checkout-summary h1 { margin-top: 1.25rem; font-size: 1.7rem; } + .payment-checkout-code { margin-top: 1.25rem; } + .payment-checkout-qr, .payment-checkout-qr-placeholder { margin-top: 1.25rem; } + .payment-checkout-success { min-height: 14rem; } +} + @theme { --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; --font-serif: 'Noto Serif SC', 'Georgia', serif; diff --git a/src/routing/activeAppPageRoutes.test.ts b/src/routing/activeAppPageRoutes.test.ts index 11e141e2a..94b300a3e 100644 --- a/src/routing/activeAppPageRoutes.test.ts +++ b/src/routing/activeAppPageRoutes.test.ts @@ -30,6 +30,9 @@ describe('appPageRoutes', () => { expect(resolveSelectionStageFromPath('/games/publish')).toBe( 'game-publish', ); + expect(resolveSelectionStageFromPath('/pay/checkout-token')).toBe( + 'payment-checkout', + ); expect(resolveSelectionStageFromPath('/creation/rpg')).toBe('platform'); expect(resolveSelectionStageFromPath('/runtime/puzzle')).toBe('platform'); expect(isKnownMainAppPagePath('/creation')).toBe(true); @@ -42,6 +45,10 @@ describe('appPageRoutes', () => { expect(resolvePathForSelectionStage('games')).toBe('/games'); expect(resolvePathForSelectionStage('game-mine')).toBe('/games/mine'); expect(resolvePathForSelectionStage('game-publish')).toBe('/games/publish'); + expect(resolvePathForSelectionStage('profile-payment')).toBe( + '/profile/payment', + ); + expect(resolvePathForSelectionStage('payment-checkout')).toBe('/pay'); }); it('requires a project id for direct editor navigation', () => { diff --git a/src/routing/activeAppPageRoutes.ts b/src/routing/activeAppPageRoutes.ts index 5da7357e4..8c91e2e6e 100644 --- a/src/routing/activeAppPageRoutes.ts +++ b/src/routing/activeAppPageRoutes.ts @@ -8,6 +8,8 @@ const STAGE_ROUTE_ENTRIES = [ ['creation-home', '/creation'], ['project', '/project'], ['profile', '/profile'], + ['profile-payment', '/profile/payment'], + ['payment-checkout', '/pay'], ['image-editor', '/editor/canvas'], ['games', '/games'], ['game-detail', '/games/detail'], @@ -39,6 +41,9 @@ export function normalizeAppPath(pathname: string) { export function resolveSelectionStageFromPath( pathname: string, ): SelectionStage { + if (normalizeAppPath(pathname).startsWith('/pay/')) { + return 'payment-checkout'; + } return ROUTE_STAGE_BY_PATH.get(normalizeAppPath(pathname)) ?? 'platform'; } @@ -69,7 +74,8 @@ export function shouldRedirectEditorCanvasWithoutProject( } export function isKnownMainAppPagePath(pathname: string) { - return ROUTE_STAGE_BY_PATH.has(normalizeAppPath(pathname)); + const normalized = normalizeAppPath(pathname); + return ROUTE_STAGE_BY_PATH.has(normalized) || normalized.startsWith('/pay/'); } export function pushAppHistoryPath(path: string) { diff --git a/src/services/activeAppTitle.ts b/src/services/activeAppTitle.ts index 618713eab..d3a82f17d 100644 --- a/src/services/activeAppTitle.ts +++ b/src/services/activeAppTitle.ts @@ -9,6 +9,8 @@ const APP_TITLE_BY_SELECTION_STAGE: Record = { 'creation-home': APP_HOME_TITLE, project: '项目 - 陶泥儿', profile: '我的 - 陶泥儿', + 'profile-payment': '支付服务 - 陶泥儿', + 'payment-checkout': '扫码支付 - 陶泥儿', 'image-editor': '美术编辑器 - 陶泥儿', games: '游戏 - 陶泥儿', 'game-detail': '游戏详情 - 陶泥儿', diff --git a/src/services/payment/paymentCheckoutClient.ts b/src/services/payment/paymentCheckoutClient.ts new file mode 100644 index 000000000..0438338e0 --- /dev/null +++ b/src/services/payment/paymentCheckoutClient.ts @@ -0,0 +1,11 @@ +import type { PaymentOrderCreateResponse } from '../../../packages/shared/src/contracts/payment'; +import { requestJson } from '../apiClient'; + +export function getPublicPaymentCheckout(checkoutToken: string) { + return requestJson( + `/api/payment/checkout/${encodeURIComponent(checkoutToken)}`, + { method: 'GET' }, + '读取支付订单失败', + { skipAuth: true, skipRefresh: true }, + ); +} diff --git a/src/services/platform-entry/platformProfileClient.ts b/src/services/platform-entry/platformProfileClient.ts index 7164a8118..561674520 100644 --- a/src/services/platform-entry/platformProfileClient.ts +++ b/src/services/platform-entry/platformProfileClient.ts @@ -1,3 +1,9 @@ +import type { + PaymentApiKey, + PaymentApp, + PaymentAppCreateResponse, + PaymentAppListResponse, +} from '../../../packages/shared/src/contracts/payment'; import type { ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderResponse, @@ -140,6 +146,63 @@ export function revokePlatformProfileExternalApiKey( ); } +export function listPlatformProfilePaymentApps( + options: PlatformProfileRequestOptions = {}, +) { + return requestPlatformProfileJson( + '/payment/apps', + { method: 'GET' }, + '读取支付服务失败', + options, + ); +} + +export function createPlatformProfilePaymentApp( + name: string, + callbackUrl: string | null, + options: PlatformProfileRequestOptions = {}, +) { + return requestPlatformProfileJson( + '/payment/apps', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, callbackUrl }), + }, + '创建支付应用失败', + options, + ); +} + +export function updatePlatformProfilePaymentApp( + appId: string, + enabled: boolean, + options: PlatformProfileRequestOptions = {}, +) { + return requestPlatformProfileJson( + `/payment/apps/${encodeURIComponent(appId)}`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled }), + }, + '更新支付应用失败', + options, + ); +} + +export function revokePlatformProfilePaymentApiKey( + keyId: string, + options: PlatformProfileRequestOptions = {}, +) { + return requestPlatformProfileJson( + `/payment/keys/${encodeURIComponent(keyId)}`, + { method: 'DELETE' }, + '撤销支付 API Key 失败', + options, + ); +} + export function getPlatformProfileRechargeCenter( options: PlatformProfileRequestOptions = {}, ) {