openapi: 3.0.3 info: title: JRNY svc-subscription UniBee Integration Lab version: 0.1.0 description: | App-facing contract for the standalone integration lab. The lab accepts identity fields directly for testing. Production svc-subscription must derive them from a verified Keycloak JWT. Amounts are integer minor units (cents for CNY). servers: - url: https://chat.52smile.vip description: Aliyun integration lab - url: http://127.0.0.1:8099 description: Local lab security: - labBearer: [] tags: - name: Subscription - name: Webhook - name: Lab paths: /health: get: tags: [Lab] summary: Liveness probe security: [] responses: "200": description: Process is running content: application/json: schema: $ref: "#/components/schemas/ApiResponse" /api/plans: get: tags: [Subscription] summary: List configured purchasable plans description: Calls POST /merchant/plan/list on UniBee. responses: "200": description: Configured plan mapping and UniBee plan details content: application/json: schema: $ref: "#/components/schemas/ApiResponse" "502": $ref: "#/components/responses/UpstreamError" /api/preview: post: tags: [Subscription] summary: Preview price, discount, and tax description: Calls POST /merchant/subscription/create_preview on UniBee. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PurchaseRequest" responses: "200": description: Price preview content: application/json: schema: $ref: "#/components/schemas/PreviewResponse" "400": $ref: "#/components/responses/InvalidRequest" "422": $ref: "#/components/responses/UpstreamError" /api/checkout: post: tags: [Subscription] summary: Create a pending subscription order and APP payment action description: | Calls POST /merchant/subscription/create_submit with paymentUIMode=custom and metadata.payment_type=APP. Reuse the exact total and currency returned by preview. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CheckoutRequest" responses: "200": description: Pending order and WeChat APP SDK fields content: application/json: schema: $ref: "#/components/schemas/CheckoutResponse" "400": $ref: "#/components/responses/InvalidRequest" "409": description: Idempotency key conflict or request still in progress content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "422": $ref: "#/components/responses/UpstreamError" /api/users/{keycloakUserId}/current: get: tags: [Subscription] summary: Query current source-of-truth subscription state description: Calls POST /merchant/subscription/user_subscription_detail on UniBee. parameters: - $ref: "#/components/parameters/KeycloakUserId" responses: "200": description: Current UniBee user, subscription, plan, and gateway detail content: application/json: schema: $ref: "#/components/schemas/ApiResponse" "422": $ref: "#/components/responses/UpstreamError" /api/users/{keycloakUserId}/projection: get: tags: [Lab] summary: Read the webhook-driven local projection parameters: - $ref: "#/components/parameters/KeycloakUserId" responses: "200": description: Local subscription projection content: application/json: schema: $ref: "#/components/schemas/ApiResponse" "404": description: No local projection exists /api/refunds: post: tags: [Subscription] summary: Refund an owned successful payment description: | Queries /merchant/payment/detail first, verifies externalUserId ownership, refundable amount, currency, and payment status, then calls POST /merchant/payment/refund/new. A refund does not revoke entitlement. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RefundRequest" responses: "200": description: Refund request accepted or cached idempotent result content: application/json: schema: $ref: "#/components/schemas/ApiResponse" "400": $ref: "#/components/responses/InvalidRequest" "403": description: Payment belongs to another user content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "409": description: Idempotency key conflict or request still in progress "422": $ref: "#/components/responses/UpstreamError" /api/subscriptions/cancel: post: tags: [Subscription] summary: Cancel the current owned subscription requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CancelRequest" responses: "200": description: Cancellation submitted; final state arrives by webhook content: application/json: schema: $ref: "#/components/schemas/ApiResponse" "403": description: Subscription ownership mismatch "404": description: No cancellable subscription /api/webhooks/unibee: post: tags: [Webhook] summary: Receive a signed UniBee merchant webhook description: | Signature is Base64(HMAC-SHA256(exact raw body, webhook_secret)). The receiver deduplicates by eventId and must return the exact plain-text body success. Valid unique events are asynchronously reconciled against UniBee source-of-truth APIs. security: - merchantBearer: [] webhookSignature: [] parameters: - name: X-Signature-Algorithm in: header schema: type: string enum: [hmac, hmac-sha256] - name: EventId in: header required: true schema: type: string - name: EventType in: header required: true schema: type: string - name: Msg-id in: header schema: type: string - name: Datetime in: header schema: type: string requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UniBeeWebhook" responses: "200": description: Exact acknowledgement required by UniBee content: text/plain: schema: type: string enum: [success] "400": description: Invalid body or mismatched event headers "401": description: Invalid bearer token or signature /api/state: get: tags: [Lab] summary: Read all local projections and audit records responses: "200": description: Local lab state content: application/json: schema: $ref: "#/components/schemas/ApiResponse" components: securitySchemes: labBearer: type: http scheme: bearer description: Dedicated LAB_ACCESS_TOKEN for App APIs; never reuse the UniBee merchant api_key. merchantBearer: type: http scheme: bearer description: UniBee merchant api_key sent by the webhook dispatcher. webhookSignature: type: apiKey in: header name: X-Signature description: HMAC-SHA256 signature over the exact raw request body. parameters: KeycloakUserId: name: keycloakUserId in: path required: true description: Keycloak JWT sub. The lab maps it to a UniBee externalUserId. schema: type: string maxLength: 200 responses: InvalidRequest: description: Request validation failed content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" UpstreamError: description: UniBee rejected the request or could not be reached content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" schemas: PurchaseRequest: type: object required: [keycloakUserId, email, productCode, paymentMethod] properties: keycloakUserId: type: string example: jrny-lab-user-001 email: type: string format: email maxLength: 200 example: jrny-lab-user-001@jrny.local productCode: type: string example: monthly paymentMethod: type: string example: wechatpay discountCode: type: string quantity: type: integer format: int64 minimum: 1 maximum: 100 default: 1 CheckoutRequest: allOf: - $ref: "#/components/schemas/PurchaseRequest" - type: object required: [confirmTotalAmount, confirmCurrency, idempotencyKey] properties: confirmTotalAmount: type: integer format: int64 minimum: 0 description: Exact totalAmount returned by preview. confirmCurrency: type: string minLength: 3 maxLength: 3 example: CNY idempotencyKey: type: string minLength: 1 maxLength: 128 RefundRequest: type: object additionalProperties: false required: [keycloakUserId, paymentId, refundAmount, idempotencyKey] properties: keycloakUserId: type: string paymentId: type: string refundAmount: type: integer format: int64 minimum: 1 currency: type: string minLength: 3 maxLength: 3 example: CNY reason: type: string idempotencyKey: type: string minLength: 1 maxLength: 128 CancelRequest: type: object additionalProperties: false required: [keycloakUserId, mode] properties: keycloakUserId: type: string subscriptionId: type: string description: Optional; when present it must match the owned current subscription. mode: type: string enum: [immediate, at_period_end] PreviewResponse: type: object properties: ok: type: boolean traceId: type: string data: type: object properties: identity: type: object productCode: type: string paymentMethod: type: string planId: type: integer format: int64 gatewayId: type: integer format: int64 originAmount: type: integer format: int64 discountAmount: type: integer format: int64 taxAmount: type: integer format: int64 totalAmount: type: integer format: int64 currency: type: string CheckoutResponse: type: object properties: ok: type: boolean traceId: type: string data: type: object properties: subscription: type: object paymentId: type: string invoiceId: type: string paid: type: boolean totalAmount: type: integer format: int64 currency: type: string action: $ref: "#/components/schemas/WeChatAppAction" WeChatAppAction: type: object required: [prepayId, partnerId, nonceStr, sign, timestamp, package] properties: prepayId: type: string partnerId: type: string nonceStr: type: string sign: type: string timestamp: type: string package: type: string example: Sign=WXPay UniBeeWebhook: type: object required: [eventId, eventType] properties: eventId: type: string eventType: type: string example: payment.success msgId: type: string datetime: type: string user: type: object properties: externalUserId: type: string email: type: string subscription: type: object payment: type: object invoice: type: object refund: type: object ApiResponse: type: object properties: ok: type: boolean traceId: type: string data: {} error: $ref: "#/components/schemas/ApiError" ErrorResponse: type: object required: [ok, error] properties: ok: type: boolean enum: [false] traceId: type: string error: $ref: "#/components/schemas/ApiError" ApiError: type: object properties: code: type: string message: type: string upstream: {}