From 9025e0342281d31cef5dd5addd5d2ceb5d1c3934 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 31 Aug 2026 15:57:11 +0200 Subject: [PATCH] feat(admin): remove the unfinished billing APIs and panel UI (#2248) --- fluxer_admin/openapi-admin.json | 1105 +------------ fluxer_admin/src/acl.rs | 6 - fluxer_admin/src/api/billing.rs | 154 -- fluxer_admin/src/api/generated.rs | 4 - fluxer_admin/src/api/mod.rs | 1 - fluxer_admin/src/api/types/billing.rs | 39 - fluxer_admin/src/api/types/mod.rs | 2 - fluxer_admin/src/routes/guild_tabs.rs | 16 - fluxer_admin/src/routes/user_actions.rs | 49 - fluxer_admin/src/routes/user_tabs.rs | 38 - .../pages/guild_detail_tabs/billing.rs | 94 -- .../templates/pages/guild_detail_tabs/mod.rs | 1 - .../src/templates/pages/user_detail.rs | 14 +- .../pages/user_detail_tabs/billing.rs | 410 ----- .../templates/pages/user_detail_tabs/mod.rs | 1 - .../controllers/BillingAdminController.ts | 1471 ----------------- fluxer_api/src/api/admin/controllers/index.ts | 2 - .../tests/AdminBillingAuthorization.test.ts | 62 - .../admin/tests/AdminBillingOverview.test.ts | 924 ----------- packages/constants/src/AdminACLs.ts | 3 - .../src/domains/admin/AdminBillingSchemas.ts | 172 -- 21 files changed, 6 insertions(+), 4562 deletions(-) delete mode 100644 fluxer_admin/src/api/billing.rs delete mode 100644 fluxer_admin/src/api/types/billing.rs delete mode 100644 fluxer_admin/src/templates/pages/guild_detail_tabs/billing.rs delete mode 100644 fluxer_admin/src/templates/pages/user_detail_tabs/billing.rs delete mode 100644 fluxer_api/src/api/admin/controllers/BillingAdminController.ts delete mode 100644 fluxer_api/src/api/admin/tests/AdminBillingAuthorization.test.ts delete mode 100644 fluxer_api/src/api/admin/tests/AdminBillingOverview.test.ts delete mode 100644 packages/schema/src/domains/admin/AdminBillingSchemas.ts diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index fa710e074..6daeda401 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -2278,691 +2278,6 @@ } } }, - "/admin/billing/guilds/{guildId}/overview": { - "get": { - "operationId": "admin_billing_guild_overview", - "summary": "Get billing overview for a guild", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminBillingOverviewResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve guild billing state. The current billing mirror is user-scoped, so guilds without persisted billing records return an empty overview.", - "security": [{"adminApiKey": []}], - "parameters": [ - { - "name": "guildId", - "in": "path", - "required": true, - "schema": {"type": "string"}, - "description": "The guildId" - } - ] - } - }, - "/admin/billing/users/{userId}/cancel-subscription": { - "post": { - "operationId": "admin_billing_cancel_subscription", - "summary": "Cancel a user subscription", - "tags": ["Admin"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Set a user Stripe subscription to cancel at period end.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/cancel-subscription-now": { - "post": { - "operationId": "admin_billing_cancel_subscription_now", - "summary": "Cancel a user subscription immediately", - "tags": ["Admin"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Cancel a user Stripe subscription immediately without issuing a refund.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ], - "requestBody": { - "required": true, - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/AdminBillingCancelImmediatelyRequest"}} - } - } - } - }, - "/admin/billing/users/{userId}/end-premium-grace-period": { - "post": { - "operationId": "admin_billing_end_premium_grace_period", - "summary": "End a user's premium grace period", - "tags": ["Admin"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "End the post-cancel premium grace period for a user immediately, downgrading them and clearing premium_since. Idempotent: safe to call when not in grace. Use when investigating fraud or honoring a user request to opt out of the recovery window.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/invoices": { - "get": { - "operationId": "admin_billing_list_invoices", - "summary": "List invoices for a user", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminInvoiceListResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve recent Stripe invoices for a user.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/overview": { - "get": { - "operationId": "admin_billing_overview", - "summary": "Get billing overview for a user", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminBillingOverviewResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve subscription status, payment history, and Stripe payment methods for a user.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/payment-methods": { - "get": { - "operationId": "admin_billing_list_payment_methods", - "summary": "List payment methods for a user", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminPaymentMethodListResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve the Stripe payment methods associated with a user.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/payments": { - "get": { - "operationId": "admin_billing_list_payments", - "summary": "List payments for a user", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminPaymentListResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve the payment history stored for a user.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/reactivate-subscription": { - "post": { - "operationId": "admin_billing_reactivate_subscription", - "summary": "Reactivate a user subscription", - "tags": ["Admin"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Remove a period-end cancellation from a user Stripe subscription.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, - "/admin/billing/users/{userId}/refund": { - "post": { - "operationId": "admin_billing_refund", - "summary": "Issue a refund for a user payment", - "tags": ["Admin"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Issue a full or partial refund for a user payment through Stripe.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminBillingRefundRequest"}}} - } - } - }, - "/admin/billing/users/{userId}/refund-policy-cancel-now": { - "post": { - "operationId": "admin_billing_refund_policy_cancel_now", - "summary": "Apply refund policy and cancel subscription immediately", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/AdminBillingRefundLatestInvoiceCancelResponse"} - } - } - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Cancels a user subscription immediately and applies the support refund policy against the latest paid Stripe invoice.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/AdminBillingRefundLatestInvoiceCancelRequest"} - } - } - } - } - }, - "/admin/billing/users/{userId}/subscription": { - "get": { - "operationId": "admin_billing_get_subscription", - "summary": "Get subscription for a user", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminSubscriptionResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Retrieve the current Stripe subscription details for a user.", - "security": [{"adminApiKey": []}], - "parameters": [ - {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "The userId"} - ] - } - }, "/admin/bulk/add-guild-members": { "post": { "operationId": "bulk_add_guild_members", @@ -9493,7 +8808,7 @@ "acls": { "type": "array", "items": {"type": "string"}, - "maxItems": 114, + "maxItems": 111, "description": "List of access control permissions for the key" } }, @@ -9853,7 +9168,7 @@ "acls": { "type": "array", "items": {"type": "string"}, - "maxItems": 114, + "maxItems": 111, "description": "List of access control permissions for the key" } }, @@ -9883,7 +9198,7 @@ "acls": { "type": "array", "items": {"type": "string"}, - "maxItems": 114, + "maxItems": 111, "description": "List of access control permissions for the key" } }, @@ -10570,416 +9885,6 @@ }, "required": ["url"] }, - "AdminBillingOverviewResponse": { - "type": "object", - "properties": { - "subscription": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/AdminSubscriptionResponse"}]}, - "payments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "checkout_session_id": {"nullable": true, "type": "string"}, - "user_id": {"type": "string"}, - "stripe_customer_id": {"nullable": true, "type": "string"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "resolved_payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"}, - "subscription_id": {"nullable": true, "type": "string"}, - "invoice_id": {"nullable": true, "type": "string"}, - "price_id": {"nullable": true, "type": "string"}, - "product_type": {"nullable": true, "type": "string"}, - "amount_cents": {"type": "number"}, - "currency": {"type": "string"}, - "status": {"type": "string"}, - "stripe_source": {"enum": ["invoice"], "type": "string"}, - "refundable_via_payment_intent": {"type": "boolean"}, - "refunded_amount_cents": {"type": "number"}, - "net_amount_cents": {"type": "number"}, - "refunds": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "amount_cents": {"type": "number"}, - "currency": {"type": "string"}, - "status": {"nullable": true, "type": "string"}, - "reason": {"nullable": true, "type": "string"}, - "created": {"type": "number"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"} - }, - "required": [ - "id", - "amount_cents", - "currency", - "status", - "reason", - "created", - "payment_intent_id", - "charge_id" - ] - } - }, - "payment_method_type": {"nullable": true, "type": "string"}, - "payment_method_brand": {"nullable": true, "type": "string"}, - "payment_method_last4": {"nullable": true, "type": "string"}, - "stripe_payment_method_country_code": {"nullable": true, "type": "string"}, - "stripe_billing_country_code": {"nullable": true, "type": "string"}, - "stripe_customer_country_code": {"nullable": true, "type": "string"}, - "stripe_terms_of_service_accepted": {"nullable": true, "type": "boolean"}, - "is_gift": {"type": "boolean"}, - "gift_code": {"nullable": true, "type": "string"}, - "purchase_geoip_country_code": {"nullable": true, "type": "string"}, - "purchase_client_country_code": {"nullable": true, "type": "string"}, - "eu_withdrawal_waiver_required": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted_at": {"nullable": true, "type": "string"}, - "eu_withdrawal_waiver_text_version": {"nullable": true, "type": "string"}, - "created_at": {"type": "string"}, - "completed_at": {"nullable": true, "type": "string"} - }, - "required": [ - "checkout_session_id", - "user_id", - "stripe_customer_id", - "payment_intent_id", - "resolved_payment_intent_id", - "charge_id", - "subscription_id", - "invoice_id", - "price_id", - "product_type", - "amount_cents", - "currency", - "status", - "stripe_source", - "refundable_via_payment_intent", - "refunded_amount_cents", - "net_amount_cents", - "refunds", - "payment_method_type", - "payment_method_brand", - "payment_method_last4", - "stripe_payment_method_country_code", - "stripe_billing_country_code", - "stripe_customer_country_code", - "stripe_terms_of_service_accepted", - "is_gift", - "gift_code", - "purchase_geoip_country_code", - "purchase_client_country_code", - "eu_withdrawal_waiver_required", - "eu_withdrawal_waiver_accepted", - "eu_withdrawal_waiver_accepted_at", - "eu_withdrawal_waiver_text_version", - "created_at", - "completed_at" - ] - } - }, - "payment_methods": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "type": {"type": "string"}, - "card_brand": {"nullable": true, "type": "string"}, - "card_last4": {"nullable": true, "type": "string"}, - "card_exp_month": {"nullable": true, "type": "number"}, - "card_exp_year": {"nullable": true, "type": "number"}, - "created": {"type": "number"} - }, - "required": ["id", "type", "card_brand", "card_last4", "card_exp_month", "card_exp_year", "created"] - } - }, - "stripe_customer_id": {"nullable": true, "type": "string"} - }, - "required": ["subscription", "payments", "payment_methods", "stripe_customer_id"] - }, - "AdminSubscriptionResponse": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "status": {"type": "string"}, - "current_period_start": {"nullable": true, "type": "string"}, - "current_period_end": {"nullable": true, "type": "string"}, - "cancel_at_period_end": {"type": "boolean"}, - "cancel_at": {"nullable": true, "type": "string"}, - "canceled_at": {"nullable": true, "type": "string"}, - "plan_interval": {"nullable": true, "type": "string"}, - "plan_amount_cents": {"nullable": true, "type": "number"}, - "plan_currency": {"nullable": true, "type": "string"}, - "default_payment_method_id": {"nullable": true, "type": "string"} - }, - "required": [ - "id", - "status", - "current_period_start", - "current_period_end", - "cancel_at_period_end", - "cancel_at", - "canceled_at", - "plan_interval", - "plan_amount_cents", - "plan_currency", - "default_payment_method_id" - ] - }, - "AdminBillingCancelImmediatelyRequest": { - "type": "object", - "properties": {"reason": {"type": "string", "minLength": 1, "maxLength": 512}} - }, - "AdminInvoiceListResponse": { - "type": "object", - "properties": { - "invoices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "amount_due": {"type": "number"}, - "amount_paid": {"type": "number"}, - "currency": {"type": "string"}, - "status": {"nullable": true, "type": "string"}, - "created": {"type": "number"}, - "billing_reason": {"nullable": true, "type": "string"}, - "subscription_id": {"nullable": true, "type": "string"}, - "payment_type": {"nullable": true, "type": "string"}, - "payment_status": {"nullable": true, "type": "string"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"}, - "paid_at": {"nullable": true, "type": "string"}, - "hosted_invoice_url": {"nullable": true, "type": "string"}, - "invoice_pdf": {"nullable": true, "type": "string"} - }, - "required": [ - "id", - "amount_due", - "amount_paid", - "currency", - "status", - "created", - "billing_reason", - "subscription_id", - "payment_type", - "payment_status", - "payment_intent_id", - "charge_id", - "paid_at", - "hosted_invoice_url", - "invoice_pdf" - ] - } - }, - "has_more": {"type": "boolean"} - }, - "required": ["invoices", "has_more"] - }, - "AdminPaymentMethodListResponse": { - "type": "object", - "properties": { - "payment_methods": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "type": {"type": "string"}, - "card_brand": {"nullable": true, "type": "string"}, - "card_last4": {"nullable": true, "type": "string"}, - "card_exp_month": {"nullable": true, "type": "number"}, - "card_exp_year": {"nullable": true, "type": "number"}, - "created": {"type": "number"} - }, - "required": ["id", "type", "card_brand", "card_last4", "card_exp_month", "card_exp_year", "created"] - } - } - }, - "required": ["payment_methods"] - }, - "AdminPaymentListResponse": { - "type": "object", - "properties": { - "payments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "checkout_session_id": {"nullable": true, "type": "string"}, - "user_id": {"type": "string"}, - "stripe_customer_id": {"nullable": true, "type": "string"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "resolved_payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"}, - "subscription_id": {"nullable": true, "type": "string"}, - "invoice_id": {"nullable": true, "type": "string"}, - "price_id": {"nullable": true, "type": "string"}, - "product_type": {"nullable": true, "type": "string"}, - "amount_cents": {"type": "number"}, - "currency": {"type": "string"}, - "status": {"type": "string"}, - "stripe_source": {"enum": ["invoice"], "type": "string"}, - "refundable_via_payment_intent": {"type": "boolean"}, - "refunded_amount_cents": {"type": "number"}, - "net_amount_cents": {"type": "number"}, - "refunds": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "amount_cents": {"type": "number"}, - "currency": {"type": "string"}, - "status": {"nullable": true, "type": "string"}, - "reason": {"nullable": true, "type": "string"}, - "created": {"type": "number"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"} - }, - "required": [ - "id", - "amount_cents", - "currency", - "status", - "reason", - "created", - "payment_intent_id", - "charge_id" - ] - } - }, - "payment_method_type": {"nullable": true, "type": "string"}, - "payment_method_brand": {"nullable": true, "type": "string"}, - "payment_method_last4": {"nullable": true, "type": "string"}, - "stripe_payment_method_country_code": {"nullable": true, "type": "string"}, - "stripe_billing_country_code": {"nullable": true, "type": "string"}, - "stripe_customer_country_code": {"nullable": true, "type": "string"}, - "stripe_terms_of_service_accepted": {"nullable": true, "type": "boolean"}, - "is_gift": {"type": "boolean"}, - "gift_code": {"nullable": true, "type": "string"}, - "purchase_geoip_country_code": {"nullable": true, "type": "string"}, - "purchase_client_country_code": {"nullable": true, "type": "string"}, - "eu_withdrawal_waiver_required": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted_at": {"nullable": true, "type": "string"}, - "eu_withdrawal_waiver_text_version": {"nullable": true, "type": "string"}, - "created_at": {"type": "string"}, - "completed_at": {"nullable": true, "type": "string"} - }, - "required": [ - "checkout_session_id", - "user_id", - "stripe_customer_id", - "payment_intent_id", - "resolved_payment_intent_id", - "charge_id", - "subscription_id", - "invoice_id", - "price_id", - "product_type", - "amount_cents", - "currency", - "status", - "stripe_source", - "refundable_via_payment_intent", - "refunded_amount_cents", - "net_amount_cents", - "refunds", - "payment_method_type", - "payment_method_brand", - "payment_method_last4", - "stripe_payment_method_country_code", - "stripe_billing_country_code", - "stripe_customer_country_code", - "stripe_terms_of_service_accepted", - "is_gift", - "gift_code", - "purchase_geoip_country_code", - "purchase_client_country_code", - "eu_withdrawal_waiver_required", - "eu_withdrawal_waiver_accepted", - "eu_withdrawal_waiver_accepted_at", - "eu_withdrawal_waiver_text_version", - "created_at", - "completed_at" - ] - } - } - }, - "required": ["payments"] - }, - "AdminBillingRefundRequest": { - "type": "object", - "properties": { - "payment_intent_id": {"type": "string"}, - "amount_cents": { - "type": "integer", - "maximum": 9007199254740991, - "format": "int53", - "minimum": 0, - "exclusiveMinimum": true - }, - "reason": {"type": "string", "minLength": 1, "maxLength": 512} - }, - "required": ["payment_intent_id"] - }, - "AdminBillingRefundLatestInvoiceCancelResponse": { - "type": "object", - "properties": { - "subscription_id": {"type": "string"}, - "invoice_id": {"type": "string"}, - "payment_intent_id": {"nullable": true, "type": "string"}, - "charge_id": {"nullable": true, "type": "string"}, - "refund_policy": {"enum": ["full_refund", "prorated_refund", "cancel_only"], "type": "string"}, - "refund_policy_basis": {"enum": ["support_policy", "eu_eea_withdrawal_no_waiver"], "type": "string"}, - "refund_id": {"nullable": true, "type": "string"}, - "refunded_amount_cents": {"type": "number"}, - "invoice_amount_paid_cents": {"type": "number"}, - "currency": {"type": "string"}, - "cycle_elapsed_days": {"type": "number"}, - "purchase_geoip_country_code": {"nullable": true, "type": "string"}, - "purchase_client_country_code": {"nullable": true, "type": "string"}, - "stripe_payment_method_country_code": {"nullable": true, "type": "string"}, - "stripe_billing_country_code": {"nullable": true, "type": "string"}, - "stripe_customer_country_code": {"nullable": true, "type": "string"}, - "stripe_terms_of_service_accepted": {"nullable": true, "type": "boolean"}, - "eu_withdrawal_waiver_required": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted": {"type": "boolean"}, - "eu_withdrawal_waiver_accepted_at": {"nullable": true, "type": "string"}, - "eu_withdrawal_waiver_text_version": {"nullable": true, "type": "string"} - }, - "required": [ - "subscription_id", - "invoice_id", - "payment_intent_id", - "charge_id", - "refund_policy", - "refund_policy_basis", - "refund_id", - "refunded_amount_cents", - "invoice_amount_paid_cents", - "currency", - "cycle_elapsed_days", - "purchase_geoip_country_code", - "purchase_client_country_code", - "stripe_payment_method_country_code", - "stripe_billing_country_code", - "stripe_customer_country_code", - "stripe_terms_of_service_accepted", - "eu_withdrawal_waiver_required", - "eu_withdrawal_waiver_accepted", - "eu_withdrawal_waiver_accepted_at", - "eu_withdrawal_waiver_text_version" - ] - }, - "AdminBillingRefundLatestInvoiceCancelRequest": { - "type": "object", - "properties": {"reason": {"type": "string", "minLength": 1, "maxLength": 512}} - }, "BulkAddGuildMembersRequest": { "type": "object", "properties": { @@ -14693,7 +13598,7 @@ "pending_bulk_message_deletion_at": {"nullable": true, "type": "string"}, "deletion_reason_code": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, "deletion_public_reason": {"nullable": true, "type": "string"}, - "acls": {"type": "array", "items": {"type": "string"}, "maxItems": 114}, + "acls": {"type": "array", "items": {"type": "string"}, "maxItems": 111}, "traits": {"type": "array", "items": {"type": "string"}, "maxItems": 100}, "has_totp": {"type": "boolean"}, "authenticator_types": {"type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}, "maxItems": 10}, @@ -15261,7 +14166,7 @@ "acls": { "type": "array", "items": {"type": "string"}, - "maxItems": 114, + "maxItems": 111, "description": "List of access control permissions to assign" } }, diff --git a/fluxer_admin/src/acl.rs b/fluxer_admin/src/acl.rs index 6a172aa7e..1a0e45ed8 100644 --- a/fluxer_admin/src/acl.rs +++ b/fluxer_admin/src/acl.rs @@ -41,9 +41,6 @@ pub const BAN_AVATAR_HASH_REMOVE: &str = "ban:avatar_hash:remove"; pub const BAN_PROFILE_SUBSTRING_ADD: &str = "ban:profile_substring:add"; pub const BAN_PROFILE_SUBSTRING_CHECK: &str = "ban:profile_substring:check"; pub const BAN_PROFILE_SUBSTRING_REMOVE: &str = "ban:profile_substring:remove"; -pub const BILLING_MANAGE_SUBSCRIPTION: &str = "billing:manage_subscription"; -pub const BILLING_REFUND: &str = "billing:refund"; -pub const BILLING_VIEW: &str = "billing:view"; pub const BULK_ADD_GUILD_MEMBERS: &str = "bulk:add:guild_members"; pub const BULK_DELETE_USERS: &str = "bulk:delete:users"; pub const BULK_UPDATE_GUILD_FEATURES: &str = "bulk:update:guild_features"; @@ -155,9 +152,6 @@ pub const ALL_ACLS: &[&str] = &[ BAN_PROFILE_SUBSTRING_ADD, BAN_PROFILE_SUBSTRING_CHECK, BAN_PROFILE_SUBSTRING_REMOVE, - BILLING_MANAGE_SUBSCRIPTION, - BILLING_REFUND, - BILLING_VIEW, BULK_ADD_GUILD_MEMBERS, BULK_DELETE_USERS, BULK_UPDATE_GUILD_FEATURES, diff --git a/fluxer_admin/src/api/billing.rs b/fluxer_admin/src/api/billing.rs deleted file mode 100644 index 659a93973..000000000 --- a/fluxer_admin/src/api/billing.rs +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::api::generated::types as generated_types; - -use super::client::{AdminApiClient, ApiError, ApiResult}; -use super::types::{ - BillingOverview, InvoiceListResponse, PaymentListResponse, PaymentMethodListResponse, - RefundCancelResponse, SubscriptionResponse, -}; - -impl AdminApiClient { - pub async fn get_billing_overview(&self, user_id: &str) -> ApiResult { - let response = self - .generated() - .admin_billing_overview(user_id) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - - pub async fn get_user_payments(&self, user_id: &str) -> ApiResult { - let response = self - .generated() - .admin_billing_list_payments(user_id) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - - pub async fn get_user_subscription(&self, user_id: &str) -> ApiResult { - let response = self - .generated() - .admin_billing_get_subscription(user_id) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - - pub async fn get_user_payment_methods( - &self, - user_id: &str, - ) -> ApiResult { - let response = self - .generated() - .admin_billing_list_payment_methods(user_id) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - - pub async fn get_user_invoices( - &self, - user_id: &str, - limit: u32, - starting_after: Option<&str>, - ) -> ApiResult { - let limit_str = limit.to_string(); - let mut params: Vec<(&str, &str)> = vec![("limit", &limit_str)]; - if let Some(sa) = starting_after { - params.push(("starting_after", sa)); - } - self.get( - &format!("/admin/billing/users/{user_id}/invoices"), - Some(¶ms), - ) - .await - } - - pub async fn issue_refund( - &self, - user_id: &str, - payment_intent_id: &str, - amount_cents: Option, - reason: Option<&str>, - ) -> ApiResult<()> { - let body = generated_types::AdminBillingRefundRequest { - amount_cents: amount_cents - .map(|value| crate::api::generated::nonzero_u64(value, "amount_cents")) - .transpose() - .map_err(|e| ApiError::Parse(e.to_string()))?, - payment_intent_id: payment_intent_id.to_owned(), - reason: reason - .map(generated_types::AdminBillingRefundRequestReason::try_from) - .transpose() - .map_err(|e| ApiError::Parse(e.to_string()))?, - }; - self.generated() - .admin_billing_refund(user_id, &body) - .await - .map_err(|e| self.generated_error(e))?; - Ok(()) - } - - pub async fn refund_policy_cancel_now( - &self, - user_id: &str, - reason: Option<&str>, - ) -> ApiResult { - let body = generated_types::AdminBillingRefundLatestInvoiceCancelRequest { - reason: reason - .map(generated_types::AdminBillingRefundLatestInvoiceCancelRequestReason::try_from) - .transpose() - .map_err(|e| ApiError::Parse(e.to_string()))?, - }; - let response = self - .generated() - .admin_billing_refund_policy_cancel_now(user_id, &body) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - - pub async fn cancel_subscription(&self, user_id: &str) -> ApiResult<()> { - self.generated() - .admin_billing_cancel_subscription(user_id) - .await - .map_err(|e| self.generated_error(e))?; - Ok(()) - } - - pub async fn cancel_subscription_immediately( - &self, - user_id: &str, - reason: Option<&str>, - ) -> ApiResult<()> { - let body = generated_types::AdminBillingCancelImmediatelyRequest { - reason: reason - .map(generated_types::AdminBillingCancelImmediatelyRequestReason::try_from) - .transpose() - .map_err(|e| ApiError::Parse(e.to_string()))?, - }; - self.generated() - .admin_billing_cancel_subscription_now(user_id, &body) - .await - .map_err(|e| self.generated_error(e))?; - Ok(()) - } - - pub async fn reactivate_subscription(&self, user_id: &str) -> ApiResult<()> { - self.generated() - .admin_billing_reactivate_subscription(user_id) - .await - .map_err(|e| self.generated_error(e))?; - Ok(()) - } - - pub async fn end_premium_grace_period(&self, user_id: &str) -> ApiResult<()> { - self.generated() - .admin_billing_end_premium_grace_period(user_id) - .await - .map_err(|e| self.generated_error(e))?; - Ok(()) - } -} diff --git a/fluxer_admin/src/api/generated.rs b/fluxer_admin/src/api/generated.rs index dbe3b6e33..22fa1e819 100644 --- a/fluxer_admin/src/api/generated.rs +++ b/fluxer_admin/src/api/generated.rs @@ -32,10 +32,6 @@ pub(crate) fn nonzero_u32(value: u32, field: &str) -> Result Result { - std::num::NonZeroU64::new(value).ok_or_else(|| format!("{field} must be greater than zero")) -} - #[cfg(test)] mod tests { use super::{number_to_u64, types::*}; diff --git a/fluxer_admin/src/api/mod.rs b/fluxer_admin/src/api/mod.rs index 7ba6ed0e1..5ab8efebb 100644 --- a/fluxer_admin/src/api/mod.rs +++ b/fluxer_admin/src/api/mod.rs @@ -8,7 +8,6 @@ pub mod archives; pub mod assets; pub mod audit; pub mod bans; -pub mod billing; pub mod bulk; pub mod client; pub mod codes; diff --git a/fluxer_admin/src/api/types/billing.rs b/fluxer_admin/src/api/types/billing.rs deleted file mode 100644 index 816b23a73..000000000 --- a/fluxer_admin/src/api/types/billing.rs +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct BillingOverview { - #[serde(flatten)] - pub data: serde_json::Value, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct PaymentListResponse { - #[serde(flatten)] - pub data: serde_json::Value, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct SubscriptionResponse { - #[serde(flatten)] - pub data: serde_json::Value, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct PaymentMethodListResponse { - #[serde(flatten)] - pub data: serde_json::Value, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct InvoiceListResponse { - #[serde(flatten)] - pub data: serde_json::Value, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RefundCancelResponse { - #[serde(flatten)] - pub data: serde_json::Value, -} diff --git a/fluxer_admin/src/api/types/mod.rs b/fluxer_admin/src/api/types/mod.rs index db75ffca3..70c188df9 100644 --- a/fluxer_admin/src/api/types/mod.rs +++ b/fluxer_admin/src/api/types/mod.rs @@ -4,7 +4,6 @@ mod admin_api_keys; mod applications; mod archives; mod audit; -mod billing; mod bulk; mod codes; mod common; @@ -24,7 +23,6 @@ pub use admin_api_keys::*; pub use applications::*; pub use archives::*; pub use audit::*; -pub use billing::*; pub use bulk::*; pub use codes::*; pub use common::*; diff --git a/fluxer_admin/src/routes/guild_tabs.rs b/fluxer_admin/src/routes/guild_tabs.rs index 2c05e30d5..9137cb372 100644 --- a/fluxer_admin/src/routes/guild_tabs.rs +++ b/fluxer_admin/src/routes/guild_tabs.rs @@ -150,22 +150,6 @@ pub async fn render( }, )) } - "billing" => { - if config.self_hosted || !acl::has_permission(admin_acls, acl::BILLING_VIEW) { - return None; - } - let billing = client - .get_billing_overview(guild_id) - .await - .log_error("load guild billing overview") - .map(|b| b.data); - Some(tabs::billing::billing_tab( - config, - guild_id, - billing.as_ref(), - csrf_token, - )) - } "applications" => { if !acl::has_any_permission( admin_acls, diff --git a/fluxer_admin/src/routes/user_actions.rs b/fluxer_admin/src/routes/user_actions.rs index 5449ddbd2..f285e8126 100644 --- a/fluxer_admin/src/routes/user_actions.rs +++ b/fluxer_admin/src/routes/user_actions.rs @@ -399,55 +399,6 @@ pub async fn dispatch( "Bulk message deletion cancelled successfully", "Failed to cancel bulk message deletion", ), - "refund_payment" => { - let Some(pi) = form.clean("payment_intent_id") else { - return DispatchOutcome::error("Payment intent ID is required"); - }; - let amt = form.parse_u64("amount_cents"); - let reason = get("reason"); - DispatchOutcome::from_result( - client - .issue_refund(user_id, &pi, amt, reason.as_deref()) - .await, - "Refund issued successfully", - "Failed to issue refund", - ) - } - "refund_policy_cancel_now" => { - let reason = get("reason"); - DispatchOutcome::from_result( - client - .refund_policy_cancel_now(user_id, reason.as_deref()) - .await, - "Refund policy cancellation completed successfully", - "Failed to apply refund policy cancellation", - ) - } - "cancel_subscription" => DispatchOutcome::from_result( - client.cancel_subscription(user_id).await, - "Subscription cancelled successfully", - "Failed to cancel subscription", - ), - "cancel_subscription_now" => { - let reason = get("reason"); - DispatchOutcome::from_result( - client - .cancel_subscription_immediately(user_id, reason.as_deref()) - .await, - "Subscription cancelled immediately", - "Failed to cancel subscription immediately", - ) - } - "reactivate_subscription" => DispatchOutcome::from_result( - client.reactivate_subscription(user_id).await, - "Subscription reactivated successfully", - "Failed to reactivate subscription", - ), - "end_premium_grace_period" => DispatchOutcome::from_result( - client.end_premium_grace_period(user_id).await, - "Premium grace period ended successfully", - "Failed to end premium grace period", - ), "message_shred" => { let csv = form.first("csv_data").unwrap_or_default(); match parse_message_shred_csv(csv) { diff --git a/fluxer_admin/src/routes/user_tabs.rs b/fluxer_admin/src/routes/user_tabs.rs index 7f3d5a181..d7ebf23f9 100644 --- a/fluxer_admin/src/routes/user_tabs.rs +++ b/fluxer_admin/src/routes/user_tabs.rs @@ -155,44 +155,6 @@ pub async fn render( csrf_token, )) } - "billing" => { - if config.self_hosted - || !acl::has_any_permission( - admin_acls, - &[ - acl::BILLING_VIEW, - acl::BILLING_REFUND, - acl::BILLING_MANAGE_SUBSCRIPTION, - ], - ) - { - return None; - } - let can_view_billing = acl::has_permission(admin_acls, acl::BILLING_VIEW); - let b = if can_view_billing { - client - .get_billing_overview(user_id) - .await - .log_error("load user billing overview") - } else { - None - }; - let invoices = if can_view_billing { - client - .get_user_invoices(user_id, 25, None) - .await - .log_error("load user invoices") - } else { - None - }; - Some(tabs::billing::billing_tab( - config, - user_id, - b.as_ref().map(|v| &v.data), - invoices.as_ref().map(|v| &v.data), - csrf_token, - )) - } "guilds" => { let g = client .get_user_guilds(user_id, Some(200), None, None, Some(true)) diff --git a/fluxer_admin/src/templates/pages/guild_detail_tabs/billing.rs b/fluxer_admin/src/templates/pages/guild_detail_tabs/billing.rs deleted file mode 100644 index da4a8391b..000000000 --- a/fluxer_admin/src/templates/pages/guild_detail_tabs/billing.rs +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::{ - config::AdminConfig, - templates::components::{ - form::{csrf_input, danger_button, form_actions, submit_button}, - page_container::{card_with_header, detail_row}, - }, -}; -use maud::{Markup, html}; - -pub fn billing_tab( - config: &AdminConfig, - guild_id: &str, - billing: Option<&serde_json::Value>, - csrf_token: &str, -) -> Markup { - let base = &config.base_path; - html! { - div class="space-y-6" { - @if let Some(data) = billing { - (render_billing_summary(data)) - } @else { - (card_with_header("Billing", html! { - p class="text-sm text-neutral-500" { - "No billing information available for this guild." - } - })) - } - - (card_with_header("Billing Actions", html! { - div class="space-y-4" { - form method="post" - action={(base) "/guilds/" (guild_id) "?tab=billing&action=refresh_billing"} - class="block" { - (csrf_input(csrf_token)) - (form_actions(html! { - (submit_button("Refresh Billing Data")) - })) - } - - form method="post" - action={(base) "/guilds/" (guild_id) "?tab=billing&action=cancel_subscription"} { - (csrf_input(csrf_token)) - div class="space-y-3" { - input type="text" name="reason" placeholder="Reason (optional)" - class="block w-full rounded-md border border-neutral-300 px-3 \ - py-2 text-sm shadow-sm focus:border-brand-primary \ - focus:outline-none focus:ring-1 focus:ring-brand-primary"; - (form_actions(html! { - (danger_button("Cancel Subscription")) - })) - } - } - } - })) - } - } -} - -fn render_billing_summary(data: &serde_json::Value) -> Markup { - let customer_id = data - .get("stripe_customer_id") - .and_then(|v| v.as_str()) - .unwrap_or("\u{2014}"); - let sub_status = data - .get("subscription") - .and_then(|s| s.get("status")) - .and_then(|v| v.as_str()) - .unwrap_or("none"); - let period_end = data - .get("subscription") - .and_then(|s| s.get("current_period_end")) - .and_then(|v| v.as_str()); - - html! { - (card_with_header("Summary", html! { - dl class="divide-y divide-neutral-100" { - (detail_row("Stripe Customer", html! { - span class="text-xs" { (customer_id) } - })) - (detail_row("Subscription Status", html! { - span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs \ - font-medium bg-neutral-100 text-neutral-700" { - (sub_status) - } - })) - @if let Some(end) = period_end { - (detail_row("Current Period Ends", html! { (end) })) - } - } - })) - } -} diff --git a/fluxer_admin/src/templates/pages/guild_detail_tabs/mod.rs b/fluxer_admin/src/templates/pages/guild_detail_tabs/mod.rs index 9692c96b3..57064943b 100644 --- a/fluxer_admin/src/templates/pages/guild_detail_tabs/mod.rs +++ b/fluxer_admin/src/templates/pages/guild_detail_tabs/mod.rs @@ -3,7 +3,6 @@ pub mod applications; pub mod archives; pub mod audit_log; -pub mod billing; pub mod emojis; pub mod features; pub mod members; diff --git a/fluxer_admin/src/templates/pages/user_detail.rs b/fluxer_admin/src/templates/pages/user_detail.rs index 69ea72b51..3128e35a0 100644 --- a/fluxer_admin/src/templates/pages/user_detail.rs +++ b/fluxer_admin/src/templates/pages/user_detail.rs @@ -22,7 +22,6 @@ use maud::{Markup, html}; pub const USER_TABS: &[(&str, &str)] = &[ ("overview", "Overview"), ("account", "Account"), - ("billing", "Billing"), ("guilds", "Guilds"), ("dm_history", "DM History"), ("group_dms", "Group DMs"), @@ -164,21 +163,10 @@ fn render_user_detail( } } -fn user_tab_visible(config: &AdminConfig, tab_id: &str, admin_acls: &[String]) -> bool { +fn user_tab_visible(_config: &AdminConfig, tab_id: &str, admin_acls: &[String]) -> bool { match tab_id { "overview" | "account" | "guilds" | "dm_history" | "group_dms" | "reports" | "moderation" => true, - "billing" => { - !config.self_hosted - && acl::has_any_permission( - admin_acls, - &[ - acl::BILLING_VIEW, - acl::BILLING_REFUND, - acl::BILLING_MANAGE_SUBSCRIPTION, - ], - ) - } "relationships" => acl::has_permission(admin_acls, acl::USER_LIST_RELATIONSHIPS), "applications" => acl::has_permission(admin_acls, acl::APPLICATION_LIST_BY_OWNER), "archives" => acl::has_any_permission( diff --git a/fluxer_admin/src/templates/pages/user_detail_tabs/billing.rs b/fluxer_admin/src/templates/pages/user_detail_tabs/billing.rs deleted file mode 100644 index 878645856..000000000 --- a/fluxer_admin/src/templates/pages/user_detail_tabs/billing.rs +++ /dev/null @@ -1,410 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::{ - config::AdminConfig, - templates::components::{ - badge::{BadgeVariant, badge}, - form::{csrf_input, danger_button, form_actions, submit_button}, - page_container::{card_with_header, detail_row}, - }, -}; -use maud::{Markup, html}; - -const INPUT_CLS: &str = "block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm \ - shadow-sm focus:border-brand-primary focus:outline-none focus:ring-1 \ - focus:ring-brand-primary"; - -pub fn billing_tab( - config: &AdminConfig, - user_id: &str, - billing: Option<&serde_json::Value>, - invoices: Option<&serde_json::Value>, - csrf_token: &str, -) -> Markup { - let base = &config.base_path; - html! { - div class="space-y-6" { - @if let Some(data) = billing { - (render_billing_summary(data)) - (render_subscription(data)) - (render_payment_methods(data)) - (render_payments(data)) - } @else { - (card_with_header("Billing", html! { - p class="text-sm text-neutral-500" { - "No billing information available for this user." - } - })) - } - @if let Some(data) = invoices { - (render_invoices(data)) - } - - (render_actions(base, user_id, csrf_token)) - } - } -} - -fn subscription_badge_variant(status: &str) -> BadgeVariant { - match status { - "active" | "trialing" => BadgeVariant::Success, - "past_due" | "unpaid" | "incomplete" => BadgeVariant::Warning, - "canceled" | "incomplete_expired" => BadgeVariant::Danger, - _ => BadgeVariant::Default, - } -} - -fn render_billing_summary(data: &serde_json::Value) -> Markup { - let customer_id = data - .get("stripe_customer_id") - .and_then(|v| v.as_str()) - .unwrap_or("\u{2014}"); - let sub_status = data - .get("subscription") - .and_then(|s| s.get("status")) - .and_then(|v| v.as_str()); - let period_end = data - .get("subscription") - .and_then(|s| s.get("current_period_end")) - .and_then(|v| v.as_str()); - - html! { - (card_with_header("Summary", html! { - dl class="divide-y divide-neutral-100" { - (detail_row("Stripe Customer", html! { - span class="text-xs" { (customer_id) } - })) - (detail_row("Subscription", html! { - @if let Some(status) = sub_status { - (badge(status, subscription_badge_variant(status))) - } @else { - span class="text-sm text-neutral-900" { "none" } - } - })) - @if let Some(end) = period_end { - (detail_row("Current Period Ends", html! { (end) })) - } - } - })) - } -} - -fn render_subscription(data: &serde_json::Value) -> Markup { - let sub = match data.get("subscription") { - Some(s) if !s.is_null() => s, - _ => return html! {}, - }; - let status = sub - .get("status") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - let sub_id = sub.get("id").and_then(|v| v.as_str()); - let plan_interval = sub.get("plan_interval").and_then(|v| v.as_str()); - let period_start = sub.get("current_period_start").and_then(|v| v.as_str()); - let period_end = sub.get("current_period_end").and_then(|v| v.as_str()); - let cancel_at_period_end = sub - .get("cancel_at_period_end") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - html! { - (card_with_header("Subscription", html! { - dl class="divide-y divide-neutral-100" { - (detail_row("Status", html! { - (badge(status, subscription_badge_variant(status))) - })) - @if let Some(id) = sub_id { - (detail_row("ID", html! { - span class="text-xs" { (id) } - })) - } - @if let Some(interval) = plan_interval { - (detail_row("Plan Interval", html! { (interval) })) - } - @if let Some(start) = period_start { - (detail_row("Period Start", html! { (start) })) - } - @if let Some(end) = period_end { - (detail_row("Period End", html! { (end) })) - } - (detail_row("Cancel at Period End", html! { - @if cancel_at_period_end { "yes" } @else { "no" } - })) - } - })) - } -} - -fn render_payment_methods(data: &serde_json::Value) -> Markup { - let methods = data.get("payment_methods").and_then(|v| v.as_array()); - let empty = methods.is_none() || methods.is_some_and(|m| m.is_empty()); - html! { - (card_with_header("Payment Methods", html! { - @if empty { - p class="text-sm text-neutral-500" { "No payment methods on file." } - } @else if let Some(pms) = methods { - div class="space-y-3" { - @for pm in pms { - @let pm_type = pm.get("type").and_then(|v| v.as_str()).unwrap_or("unknown"); - @let brand = pm.get("card_brand").and_then(|v| v.as_str()); - @let last4 = pm.get("card_last4").and_then(|v| v.as_str()); - @let pm_id = pm.get("id").and_then(|v| v.as_str()).unwrap_or(""); - @let display = match (brand, last4) { - (Some(b), Some(l)) => format!("{b} **** {l}"), - _ => pm_type.to_string(), - }; - div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3" { - p class="text-sm text-neutral-900" { (display) } - p class="text-xs text-neutral-500" { (pm_id) } - } - } - } - } - })) - } -} - -fn render_payments(data: &serde_json::Value) -> Markup { - let payments = data.get("payments").and_then(|v| v.as_array()); - let empty = payments.is_none() || payments.is_some_and(|p| p.is_empty()); - html! { - (card_with_header("Payments", html! { - @if empty { - p class="text-sm text-neutral-500" { "No payments recorded." } - } @else if let Some(ps) = payments { - div class="space-y-3" { - @for p in ps { (payment_row(p)) } - } - } - })) - } -} - -fn payment_row(p: &serde_json::Value) -> Markup { - let amount = p.get("amount_cents").and_then(|v| v.as_i64()).unwrap_or(0); - let currency = p.get("currency").and_then(|v| v.as_str()).unwrap_or(""); - let status = p - .get("status") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - let created = p.get("created_at").and_then(|v| v.as_str()).unwrap_or(""); - let display_amount = format!("{:.2} {}", amount as f64 / 100.0, currency.to_uppercase()); - - let variant = match status { - "completed" | "succeeded" => BadgeVariant::Success, - "pending" | "processing" => BadgeVariant::Info, - "failed" | "canceled" => BadgeVariant::Danger, - "refunded" | "partially_refunded" => BadgeVariant::Warning, - _ => BadgeVariant::Default, - }; - - html! { - div class="rounded-lg border border-neutral-200 bg-neutral-50 p-4" { - div class="flex items-center justify-between" { - div class="flex items-center gap-2" { - span class="text-sm font-medium text-neutral-900" { - (display_amount) - } - (badge(status, variant)) - } - span class="text-xs text-neutral-500" { (created) } - } - } - } -} - -fn invoice_badge_variant(status: Option<&str>) -> BadgeVariant { - match status { - Some("paid") => BadgeVariant::Success, - Some("open" | "draft") => BadgeVariant::Info, - Some("uncollectible" | "void") => BadgeVariant::Danger, - _ => BadgeVariant::Default, - } -} - -fn render_invoices(data: &serde_json::Value) -> Markup { - let invoices = data.get("invoices").and_then(|v| v.as_array()); - let empty = invoices.is_none() || invoices.is_some_and(|i| i.is_empty()); - let has_more = data - .get("has_more") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - html! { - (card_with_header("Invoices", html! { - @if empty { - p class="text-sm text-neutral-500" { "No invoices on file." } - } @else if let Some(items) = invoices { - div class="space-y-3" { - @for invoice in items { - (invoice_row(invoice)) - } - @if has_more { - p class="text-xs text-neutral-500" { - "More invoices exist beyond this list." - } - } - } - } - })) - } -} - -fn invoice_row(invoice: &serde_json::Value) -> Markup { - let amount = invoice - .get("amount_paid") - .and_then(|v| v.as_i64()) - .unwrap_or(0); - let currency = invoice - .get("currency") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let status = invoice.get("status").and_then(|v| v.as_str()); - let created = invoice - .get("created") - .and_then(|v| v.as_i64()) - .map(format_unix_timestamp) - .unwrap_or_default(); - let display_amount = format_amount(amount, currency); - let status_label = status.unwrap_or("unknown"); - let billing_reason = invoice.get("billing_reason").and_then(|v| v.as_str()); - let invoice_id = invoice.get("id").and_then(|v| v.as_str()).unwrap_or(""); - let subscription_id = invoice.get("subscription_id").and_then(|v| v.as_str()); - let payment_intent_id = invoice.get("payment_intent_id").and_then(|v| v.as_str()); - let charge_id = invoice.get("charge_id").and_then(|v| v.as_str()); - let hosted_invoice_url = invoice.get("hosted_invoice_url").and_then(|v| v.as_str()); - let invoice_pdf = invoice.get("invoice_pdf").and_then(|v| v.as_str()); - html! { - div class="rounded-lg border border-neutral-200 bg-neutral-50 p-4" { - div class="space-y-3" { - div class="flex items-center justify-between gap-3" { - div class="flex items-center gap-2" { - span class="text-sm font-medium text-neutral-900" { - (display_amount) - } - (badge(status_label, invoice_badge_variant(status))) - } - span class="text-xs text-neutral-500" { (created) } - } - @if let Some(reason) = billing_reason { - p class="text-sm text-neutral-500" { (reason) } - } - dl class="space-y-1" { - (compact_detail_row("id", invoice_id)) - @if let Some(id) = subscription_id { - (compact_detail_row("subscription", id)) - } - @if let Some(id) = payment_intent_id { - (compact_detail_row("payment_intent", id)) - } - @if let Some(id) = charge_id { - (compact_detail_row("charge", id)) - } - } - @if hosted_invoice_url.is_some() || invoice_pdf.is_some() { - div class="flex items-center gap-3 text-sm" { - @if let Some(url) = hosted_invoice_url { - a href=(url) target="_blank" rel="noreferrer noopener" - class="text-blue-600 hover:text-blue-800 hover:underline" { - "View" - } - } - @if let Some(url) = invoice_pdf { - a href=(url) target="_blank" rel="noreferrer noopener" - class="text-blue-600 hover:text-blue-800 hover:underline" { - "PDF" - } - } - } - } - } - } - } -} - -fn compact_detail_row(label: &str, value: &str) -> Markup { - html! { - div class="grid grid-cols-1 gap-1 text-xs sm:grid-cols-3" { - dt class="text-neutral-500" { (label) } - dd class="break-all text-neutral-700 sm:col-span-2" { (value) } - } - } -} - -fn format_amount(amount_minor: i64, currency: &str) -> String { - let code = currency.trim().to_uppercase(); - if code.is_empty() { - format!("{:.2}", amount_minor as f64 / 100.0) - } else { - format!("{:.2} {code}", amount_minor as f64 / 100.0) - } -} - -fn format_unix_timestamp(value: i64) -> String { - time::OffsetDateTime::from_unix_timestamp(value) - .ok() - .and_then(|ts| { - ts.format(&time::format_description::well_known::Rfc3339) - .ok() - }) - .unwrap_or_else(|| value.to_string()) -} - -fn render_actions(base: &str, user_id: &str, csrf_token: &str) -> Markup { - html! { - (card_with_header("Billing Actions", html! { - div class="space-y-4" { - form method="post" - action={(base) "/users/" (user_id) "?tab=billing&action=cancel_subscription_now"} { - (csrf_input(csrf_token)) - div class="space-y-3" { - p class="text-sm text-neutral-700" { - "Cancel subscription immediately, no refund." - } - input type="text" name="reason" placeholder="Reason (optional)" - class=(INPUT_CLS); - (form_actions(html! { - (danger_button("Cancel Now")) - })) - } - } - - form method="post" - action={(base) "/users/" (user_id) "?tab=billing&action=cancel_subscription"} { - (csrf_input(csrf_token)) - div class="space-y-3" { - p class="text-sm text-neutral-700" { - "Cancel at renewal (access until period end)." - } - (form_actions(html! { - (submit_button("Cancel at Renewal")) - })) - } - } - - form method="post" - action={(base) "/users/" (user_id) "?tab=billing&action=refund_payment"} { - (csrf_input(csrf_token)) - div class="space-y-3" { - p class="text-sm font-medium text-neutral-700" { - "Manual Refund" - } - div class="grid grid-cols-1 gap-3 sm:grid-cols-2" { - input type="text" name="payment_intent_id" - placeholder="pi_..." required - class=(INPUT_CLS); - input type="number" name="amount_cents" min="1" - placeholder="Amount cents (blank = full)" - class=(INPUT_CLS); - } - input type="text" name="reason" - placeholder="Reason (optional)" - class=(INPUT_CLS); - (form_actions(html! { - (danger_button("Refund")) - })) - } - } - } - })) - } -} diff --git a/fluxer_admin/src/templates/pages/user_detail_tabs/mod.rs b/fluxer_admin/src/templates/pages/user_detail_tabs/mod.rs index 3c5357662..de5d2beaf 100644 --- a/fluxer_admin/src/templates/pages/user_detail_tabs/mod.rs +++ b/fluxer_admin/src/templates/pages/user_detail_tabs/mod.rs @@ -5,7 +5,6 @@ use crate::{api::types::AdminResolvedUser, utils::bigint::format_discriminator}; pub mod account; pub mod applications; pub mod archives; -pub mod billing; pub mod dm_history; pub mod group_dm; pub mod guilds; diff --git a/fluxer_api/src/api/admin/controllers/BillingAdminController.ts b/fluxer_api/src/api/admin/controllers/BillingAdminController.ts deleted file mode 100644 index 561e77e04..000000000 --- a/fluxer_api/src/api/admin/controllers/BillingAdminController.ts +++ /dev/null @@ -1,1471 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; -import {isEuEeaCountryCode} from '@fluxer/constants/src/EuropeanEconomicArea'; -import {FeatureNotAvailableSelfHostedError} from '@fluxer/errors/src/domains/core/FeatureNotAvailableSelfHostedError'; -import {NotFoundError} from '@fluxer/errors/src/domains/core/NotFoundError'; -import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; -import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; -import {StripeNoActiveSubscriptionError} from '@fluxer/errors/src/domains/payment/StripeNoActiveSubscriptionError'; -import {StripePaymentNotAvailableError} from '@fluxer/errors/src/domains/payment/StripePaymentNotAvailableError'; -import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; -import { - AdminBillingCancelImmediatelyRequest, - AdminBillingOverviewResponse, - AdminBillingRefundLatestInvoiceCancelRequest, - AdminBillingRefundLatestInvoiceCancelResponse, - AdminBillingRefundRequest, - AdminInvoiceListResponse, - AdminPaymentListResponse, - AdminPaymentMethodListResponse, - AdminSubscriptionResponse, -} from '@fluxer/schema/src/domains/admin/AdminBillingSchemas'; -import type Stripe from 'stripe'; -import {createUserID, type UserID} from '../../BrandedTypes'; -import type {BillingRepository} from '../../billing/repositories/BillingRepository'; -import {Config} from '../../Config'; -import type { - BillingChargeRow, - BillingCheckoutSessionRow, - BillingInvoiceRow, - BillingPaymentIntentRow, - BillingPaymentMethodRow, - BillingPaymentRow, - BillingPriceRow, - BillingRefundRow, - BillingSubscriptionRow, -} from '../../database/types/BillingTypes'; -import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService'; -import {Logger} from '../../Logger'; -import {requireAdminACL} from '../../middleware/AdminMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {getBillingRepository} from '../../middleware/ServiceRegistry'; -import type {Payment} from '../../models/Payment'; -import type {User} from '../../models/User'; -import type {StripeService} from '../../stripe/StripeService'; -import type {HonoApp, HonoEnv} from '../../types/HonoEnv'; -import type {IUserRepository} from '../../user/IUserRepository'; -import {PaymentRepository} from '../../user/repositories/PaymentRepository'; -import {Validator} from '../../Validator'; -import {AdminRepository} from '../AdminRepository'; -import {AdminAuditService} from '../services/AdminAuditService'; - -function ensureBillingFeatureAvailable(): void { - if (Config.instance.selfHosted) { - throw new FeatureNotAvailableSelfHostedError(); - } -} - -async function getRequiredUser(userRepository: IUserRepository, userId: UserID): Promise { - const user = await userRepository.findUnique(userId); - if (!user) { - throw new UnknownUserError(); - } - return user; -} - -function buildPaymentNotFoundError(): NotFoundError { - return new NotFoundError({ - code: APIErrorCodes.NOT_FOUND, - }); -} - -interface AdminRefundPaymentIntentLookup { - findById(paymentIntentId: string): Promise; -} - -interface AdminRefundBillingLookup { - paymentIntents: AdminRefundPaymentIntentLookup; -} - -interface MirrorBillingPaymentRecord { - charge: BillingChargeRow | null; - checkoutSession: BillingCheckoutSessionRow | null; - invoice: BillingInvoiceRow; - primaryPayment: BillingPaymentRow | null; - localPayment: Payment | null; - refunds: Array; -} - -type AdminImmediateCancelRefundPolicy = 'full_refund' | 'prorated_refund' | 'cancel_only'; -type AdminImmediateCancelRefundPolicyBasis = 'support_policy' | 'eu_eea_withdrawal_no_waiver'; - -interface AdminImmediateCancelRefundTarget { - amountPaidCents: number; - chargeId: string | null; - currency: string; - invoiceId: string; - invoiceCreatedAt: Date; - paymentCompletedAt: Date | null; - paymentIntentId: string | null; - paidAt: Date | null; - purchaseGeoipCountryCode: string | null; - purchaseClientCountryCode: string | null; - euWithdrawalWaiverRequired: boolean; - euWithdrawalWaiverAccepted: boolean; - euWithdrawalWaiverAcceptedAt: Date | null; - euWithdrawalWaiverTextVersion: string | null; - stripeBillingCountryCode: string | null; - stripeCustomerCountryCode: string | null; - stripePaymentMethodCountryCode: string | null; - stripeTermsOfServiceAccepted: boolean | null; -} - -interface AdminImmediateCancelRefundDecision { - amountCents: number | null; - basis: AdminImmediateCancelRefundPolicyBasis; - cycleElapsedDays: number; - policy: AdminImmediateCancelRefundPolicy; -} - -const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; -const EU_EEA_WITHDRAWAL_REFUND_WINDOW_DAYS = 14; - -function getElapsedDays(from: Date, now: Date): number { - return Math.max(0, Math.floor((now.getTime() - from.getTime()) / MILLISECONDS_PER_DAY)); -} - -function normalizeCountryCode(value: string | null | undefined): string | null { - const normalized = value?.trim().toUpperCase(); - return normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : null; -} - -function parseStripeMetadataBoolean(value: string | null | undefined): boolean | null { - if (value === 'true') return true; - if (value === 'false') return false; - return null; -} - -function getCheckoutSessionAcceptedAt(session: BillingCheckoutSessionRow | null): Date | null { - const acceptedAt = session?.metadata?.get('eu_withdrawal_waiver_accepted_at'); - if (!acceptedAt) return null; - const date = new Date(acceptedAt); - return Number.isNaN(date.getTime()) ? null : date; -} - -function getCheckoutSessionMetadata(session: BillingCheckoutSessionRow | null): Map { - return session?.metadata ?? new Map(); -} - -function getMetadataString(map: Map, key: string): string | null { - return map.get(key) ?? null; -} - -function isManageableSubscriptionRow(row: BillingSubscriptionRow | null | undefined): row is BillingSubscriptionRow { - return Boolean(row && row.status !== 'canceled' && row.status !== 'incomplete_expired'); -} - -function getSubscriptionStatusPriority(status: BillingSubscriptionRow['status']): number { - switch (status) { - case 'active': - return 0; - case 'trialing': - return 1; - case 'past_due': - return 2; - case 'unpaid': - return 3; - case 'paused': - return 4; - case 'incomplete': - return 5; - case 'canceled': - return 6; - case 'incomplete_expired': - return 7; - default: - return 8; - } -} - -function getSubscriptionSortTimestamp(sub: BillingSubscriptionRow): number { - const ts = sub.current_period_end ?? sub.canceled_at ?? sub.cancel_at ?? sub.started_at ?? sub.stripe_created_at; - return ts ? ts.getTime() : 0; -} - -function sortSubscriptionsByRelevance(subs: Array): Array { - return [...subs].sort((left, right) => { - const statusDiff = getSubscriptionStatusPriority(left.status) - getSubscriptionStatusPriority(right.status); - if (statusDiff !== 0) return statusDiff; - return getSubscriptionSortTimestamp(right) - getSubscriptionSortTimestamp(left); - }); -} - -function getRefundAmount(refunds: Array): number { - return refunds - .filter((refund) => refund.status !== 'failed' && refund.status !== 'canceled') - .reduce((total, refund) => total + Number(refund.amount ?? 0n), 0); -} - -function buildPaymentStatus( - invoice: BillingInvoiceRow, - payment: BillingPaymentRow | null, - refunds: Array, -): string { - const refundedAmount = getRefundAmount(refunds); - const amountPaid = Number(invoice.amount_paid ?? 0n); - if (amountPaid > 0 && refundedAmount >= amountPaid) { - return 'refunded'; - } - if (refundedAmount > 0) { - return 'partially_refunded'; - } - return payment?.status ?? invoice.status ?? 'unknown'; -} - -function decideImmediateCancelRefund(params: { - amountPaidCents: number; - now: Date; - refundTarget: AdminImmediateCancelRefundTarget | null; - subscription: BillingSubscriptionRow; -}): AdminImmediateCancelRefundDecision { - const startMs = params.subscription.current_period_start?.getTime() ?? null; - const endMs = params.subscription.current_period_end?.getTime() ?? null; - const cycle = startMs && endMs && endMs > startMs ? {start: startMs, end: endMs} : null; - const cycleElapsedDays = cycle ? getElapsedDays(new Date(cycle.start), params.now) : 0; - const purchaseDate = - params.refundTarget?.paymentCompletedAt ?? - params.refundTarget?.paidAt ?? - params.refundTarget?.invoiceCreatedAt ?? - null; - if ( - params.refundTarget?.euWithdrawalWaiverRequired && - !params.refundTarget.euWithdrawalWaiverAccepted && - purchaseDate && - getElapsedDays(purchaseDate, params.now) <= EU_EEA_WITHDRAWAL_REFUND_WINDOW_DAYS - ) { - return { - amountCents: params.amountPaidCents, - basis: 'eu_eea_withdrawal_no_waiver', - cycleElapsedDays, - policy: 'full_refund', - }; - } - if (!cycle) { - return { - amountCents: params.amountPaidCents, - basis: 'support_policy', - cycleElapsedDays, - policy: 'full_refund', - }; - } - const nowMs = params.now.getTime(); - const elapsedDays = cycleElapsedDays; - if (elapsedDays <= 4) { - return { - amountCents: params.amountPaidCents, - basis: 'support_policy', - cycleElapsedDays: elapsedDays, - policy: 'full_refund', - }; - } - if (elapsedDays <= 18) { - const totalMs = Math.max(1, cycle.end - cycle.start); - const remainingMs = Math.max(0, cycle.end - nowMs); - const proratedAmount = Math.max( - 1, - Math.min(params.amountPaidCents, Math.ceil(params.amountPaidCents * (remainingMs / totalMs))), - ); - return { - amountCents: proratedAmount, - basis: 'support_policy', - cycleElapsedDays: elapsedDays, - policy: 'prorated_refund', - }; - } - return { - amountCents: null, - basis: 'support_policy', - cycleElapsedDays: elapsedDays, - policy: 'cancel_only', - }; -} - -async function assertOwnedPaymentIntentForAdminRefund( - userRepository: Pick, - billingRepository: AdminRefundBillingLookup, - targetUser: Pick, - paymentIntentId: string, -): Promise { - const payment = await userRepository.getPaymentByPaymentIntent(paymentIntentId); - if (payment) { - if (payment.userId === targetUser.id) { - return; - } - throw buildPaymentNotFoundError(); - } - if (!targetUser.stripeCustomerId) { - throw buildPaymentNotFoundError(); - } - const mirroredIntent = await billingRepository.paymentIntents.findById(paymentIntentId); - if (mirroredIntent && mirroredIntent.customer_id === targetUser.stripeCustomerId) { - return; - } - throw buildPaymentNotFoundError(); -} - -class BillingAdminControllerService { - private readonly paymentRepository = new PaymentRepository(); - - constructor( - private readonly userRepository: IUserRepository, - private readonly stripeService: StripeService | null, - private readonly auditService: AdminAuditService, - private readonly billingRepository: BillingRepository, - ) {} - - private get stripe(): Stripe | null { - return this.stripeService?.getStripe() ?? null; - } - - private async resolveCustomerIds(user: User): Promise> { - const ids = new Set(); - if (user.stripeCustomerId) { - ids.add(user.stripeCustomerId); - } - const mirroredCustomers = await this.billingRepository.customers.findByUserId(user.id); - for (const c of mirroredCustomers) { - if (!c.deleted) ids.add(c.provider_id); - } - return [...ids]; - } - - async getResolvedStripeCustomerId(user: User): Promise { - const subscription = await this.resolvePrimaryStripeSubscription(user); - if (subscription?.customer_id) return subscription.customer_id; - const ids = await this.resolveCustomerIds(user); - return ids[0] ?? user.stripeCustomerId ?? null; - } - - private async resolveStripeSubscriptions(user: User): Promise> { - const seen = new Set(); - const out: Array = []; - const add = (row: BillingSubscriptionRow | null) => { - if (row && !seen.has(row.provider_id)) { - seen.add(row.provider_id); - out.push(row); - } - }; - if (user.stripeSubscriptionId) { - add(await this.billingRepository.subscriptions.findById(user.stripeSubscriptionId)); - } - const userSubs = await this.billingRepository.subscriptions.listByUser(user.id); - for (const ref of userSubs) { - if (seen.has(ref.provider_id)) continue; - add(await this.billingRepository.subscriptions.findById(ref.provider_id)); - } - const customerIds = await this.resolveCustomerIds(user); - for (const customerId of customerIds) { - const refs = await this.billingRepository.subscriptions.listByCustomer(customerId); - for (const ref of refs) { - if (seen.has(ref.provider_id)) continue; - add(await this.billingRepository.subscriptions.findById(ref.provider_id)); - } - } - return sortSubscriptionsByRelevance(out); - } - - private async resolvePrimaryStripeSubscription(user: User): Promise { - return (await this.resolveStripeSubscriptions(user))[0] ?? null; - } - - private async resolveManageableStripeSubscription(user: User): Promise { - return (await this.resolveStripeSubscriptions(user)).find(isManageableSubscriptionRow) ?? null; - } - - private async syncResolvedStripeState( - user: User, - params: { - customerId?: string | null; - subscriptionId?: string | null; - premiumWillCancel?: boolean; - }, - ): Promise { - const patch: { - premium_will_cancel?: boolean; - stripe_customer_id?: string | null; - stripe_subscription_id?: string | null; - } = {}; - if (params.customerId !== undefined && params.customerId !== user.stripeCustomerId) { - patch.stripe_customer_id = params.customerId; - } - if (params.subscriptionId !== undefined && params.subscriptionId !== user.stripeSubscriptionId) { - patch.stripe_subscription_id = params.subscriptionId; - } - if (params.premiumWillCancel !== undefined && params.premiumWillCancel !== user.premiumWillCancel) { - patch.premium_will_cancel = params.premiumWillCancel; - } - if (Object.keys(patch).length === 0) { - return user; - } - return this.userRepository.patchUpsert(user.id, patch, user.toRow()); - } - - async getUserPayments(user: User): Promise> { - const payments = await this.paymentRepository.findPaymentsByUserId(user.id); - return payments.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime()); - } - - async getUserSubscription(user: User): Promise { - return this.resolvePrimaryStripeSubscription(user); - } - - async getUserPaymentMethods(user: User): Promise> { - const customerIds = await this.resolveCustomerIds(user); - if (customerIds.length === 0) return []; - const byId = new Map(); - for (const customerId of customerIds) { - const rows = await this.billingRepository.paymentMethods.listByCustomer(customerId); - for (const row of rows) { - byId.set(row.provider_id, row); - } - } - return [...byId.values()].sort((left, right) => { - const lt = left.stripe_created_at?.getTime() ?? 0; - const rt = right.stripe_created_at?.getTime() ?? 0; - return rt - lt; - }); - } - - async getUserInvoices( - user: User, - limit: number = 25, - ): Promise<{ - invoices: Array; - has_more: boolean; - }> { - const customerIds = await this.resolveCustomerIds(user); - if (customerIds.length === 0) return {invoices: [], has_more: false}; - const aggregated = new Map(); - let hasMore = false; - for (const customerId of customerIds) { - const result = await this.billingRepository.invoices.listByCustomer(customerId, {pageSize: 100}); - for (const inv of result.rows) { - aggregated.set(inv.provider_id, inv); - } - if (result.pageState) hasMore = true; - } - const merged = [...aggregated.values()].sort((left, right) => { - const lt = left.stripe_created_at?.getTime() ?? 0; - const rt = right.stripe_created_at?.getTime() ?? 0; - return rt - lt; - }); - return { - invoices: merged.slice(0, limit), - has_more: hasMore || merged.length > limit, - }; - } - - private async getRefundsForInvoiceAndCharge(params: { - invoiceId: string; - chargeId: string | null; - paymentIntentId: string | null; - }): Promise> { - const byId = new Map(); - const add = (rows: Array) => { - for (const row of rows) byId.set(row.provider_id, row); - }; - add(await this.billingRepository.refunds.listByInvoice(params.invoiceId)); - if (params.chargeId) { - add(await this.billingRepository.refunds.listByCharge(params.chargeId)); - } - if (params.paymentIntentId) { - add(await this.billingRepository.refunds.listByPaymentIntent(params.paymentIntentId)); - } - return [...byId.values()]; - } - - private async getCheckoutSessionForInvoice(params: { - paymentIntentId: string | null; - subscriptionId: string | null; - customerIds: Array; - }): Promise { - for (const customerId of params.customerIds) { - const result = await this.billingRepository.checkoutSessions.listByCustomer(customerId, {pageSize: 100}); - for (const ref of result.rows) { - const session = await this.billingRepository.checkoutSessions.findById(ref.provider_id); - if (!session) continue; - if (params.paymentIntentId && session.payment_intent_id === params.paymentIntentId) { - return session; - } - if (params.subscriptionId && session.subscription_id === params.subscriptionId) { - return session; - } - } - } - return null; - } - - async getMirrorBillingPaymentRecords(user: User, limit: number = 25): Promise> { - const {invoices} = await this.getUserInvoices(user, limit); - const customerIds = await this.resolveCustomerIds(user); - const localPayments = await this.getUserPayments(user); - const localPaymentsByInvoiceId = new Map(); - for (const lp of localPayments) { - if (lp.invoiceId && !localPaymentsByInvoiceId.has(lp.invoiceId)) { - localPaymentsByInvoiceId.set(lp.invoiceId, lp); - } - } - const records = await Promise.all( - invoices.map(async (invoice): Promise => { - const primaryPayment = await this.billingRepository.payments.findPrimaryForInvoice(invoice.provider_id); - const chargeId = primaryPayment?.charge_id ?? null; - const paymentIntentId = primaryPayment?.payment_intent_id ?? null; - const charge = chargeId ? await this.billingRepository.charges.findById(chargeId) : null; - const refunds = await this.getRefundsForInvoiceAndCharge({ - invoiceId: invoice.provider_id, - chargeId, - paymentIntentId, - }); - const checkoutSession = await this.getCheckoutSessionForInvoice({ - paymentIntentId, - subscriptionId: invoice.subscription_id, - customerIds, - }); - return { - charge, - checkoutSession, - invoice, - primaryPayment, - localPayment: invoice.provider_id ? (localPaymentsByInvoiceId.get(invoice.provider_id) ?? null) : null, - refunds, - }; - }), - ); - return records.filter((record) => { - const amountPaid = Number(record.invoice.amount_paid ?? 0n); - return amountPaid > 0 || record.refunds.length > 0 || record.primaryPayment; - }); - } - - private buildRefundTargetFromMirror( - invoice: BillingInvoiceRow, - primaryPayment: BillingPaymentRow | null, - charge: BillingChargeRow | null, - checkoutSession: BillingCheckoutSessionRow | null, - expectedCustomerId: string | null, - localPayment: Payment | null, - ): AdminImmediateCancelRefundTarget | null { - if (!invoice.provider_id || invoice.status !== 'paid') return null; - const amountPaid = Number(invoice.amount_paid ?? 0n); - if (amountPaid <= 0) return null; - if (expectedCustomerId && invoice.customer_id && invoice.customer_id !== expectedCustomerId) return null; - if (!primaryPayment?.payment_intent_id && !primaryPayment?.charge_id) return null; - const sessionMetadata = getCheckoutSessionMetadata(checkoutSession); - const stripePaymentMethodCountryCode = normalizeCountryCode(charge?.card_country ?? null); - const stripeBillingCountryCode: string | null = null; - const stripeCustomerCountryCode: string | null = null; - const stripeTermsOfServiceAccepted: boolean | null = null; - const metadataWaiverRequired = parseStripeMetadataBoolean( - getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_required'), - ); - const metadataWaiverAccepted = parseStripeMetadataBoolean( - getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_accepted'), - ); - const stripeCountryRequiresWaiver = [ - stripeBillingCountryCode, - stripeCustomerCountryCode, - stripePaymentMethodCountryCode, - ].some((countryCode) => isEuEeaCountryCode(countryCode)); - const euWithdrawalWaiverRequired = - (localPayment?.euWithdrawalWaiverRequired ?? false) || - metadataWaiverRequired === true || - stripeCountryRequiresWaiver; - const euWithdrawalWaiverAccepted = - (localPayment?.euWithdrawalWaiverAccepted ?? false) || metadataWaiverAccepted === true; - return { - amountPaidCents: amountPaid, - chargeId: primaryPayment.charge_id ?? null, - currency: invoice.currency ?? 'usd', - invoiceId: invoice.provider_id, - invoiceCreatedAt: invoice.stripe_created_at ?? new Date(0), - paymentCompletedAt: localPayment?.completedAt ?? null, - paymentIntentId: primaryPayment.payment_intent_id ?? null, - paidAt: primaryPayment.paid_at ?? invoice.paid_at ?? null, - purchaseGeoipCountryCode: - localPayment?.purchaseGeoipCountryCode ?? - normalizeCountryCode(getMetadataString(sessionMetadata, 'purchase_geoip_country_code')), - purchaseClientCountryCode: - localPayment?.purchaseClientCountryCode ?? - normalizeCountryCode(getMetadataString(sessionMetadata, 'purchase_client_country_code')), - euWithdrawalWaiverRequired, - euWithdrawalWaiverAccepted, - euWithdrawalWaiverAcceptedAt: - localPayment?.euWithdrawalWaiverAcceptedAt ?? getCheckoutSessionAcceptedAt(checkoutSession), - euWithdrawalWaiverTextVersion: - localPayment?.euWithdrawalWaiverTextVersion ?? - getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_text_version'), - stripeBillingCountryCode, - stripeCustomerCountryCode, - stripePaymentMethodCountryCode, - stripeTermsOfServiceAccepted, - }; - } - - async issueRefund(params: { - adminUserId: UserID; - targetUser: User; - paymentIntentId: string; - amountCents?: number; - reason?: string; - }): Promise { - if (!this.stripe) { - throw new StripePaymentNotAvailableError(); - } - const customerId = await this.getResolvedStripeCustomerId(params.targetUser); - const syncedTargetUser = customerId - ? await this.syncResolvedStripeState(params.targetUser, {customerId}) - : params.targetUser; - await assertOwnedPaymentIntentForAdminRefund( - this.userRepository, - this.billingRepository, - syncedTargetUser, - params.paymentIntentId, - ); - try { - const refund = await this.stripe.refunds.create({ - payment_intent: params.paymentIntentId, - ...(params.amountCents !== undefined ? {amount: params.amountCents} : {}), - ...(params.reason ? {reason: 'requested_by_customer' as const} : {}), - metadata: { - admin_user_id: params.adminUserId.toString(), - target_user_id: syncedTargetUser.id.toString(), - ...(params.reason ? {admin_reason: params.reason} : {}), - }, - }); - try { - await this.billingRepository.refunds.upsertFromStripe(refund, { - customerId: syncedTargetUser.stripeCustomerId ?? undefined, - userId: syncedTargetUser.id, - }); - } catch (mirrorErr) { - Logger.error({mirrorErr, refundId: refund.id}, 'Mirror upsert failed after admin refund; reconciler will heal'); - } - } catch (error) { - throw new StripeError(error instanceof Error ? error.message : 'Failed to refund payment'); - } - const metadata = new Map([['payment_intent_id', params.paymentIntentId]]); - if (params.amountCents !== undefined) { - metadata.set('amount_cents', String(params.amountCents)); - } - if (params.reason) { - metadata.set('reason', params.reason); - } - await this.auditService.createAuditLog({ - adminUserId: params.adminUserId, - targetType: 'user', - targetId: BigInt(syncedTargetUser.id), - action: 'billing_refund', - auditLogReason: params.reason ?? null, - metadata, - }); - } - - async resolveLatestRefundableInvoiceForImmediateCancel( - user: User, - subscription: BillingSubscriptionRow, - ): Promise { - const localPayments = await this.getUserPayments(user); - const localPaymentsByInvoiceId = new Map(); - for (const payment of localPayments) { - if (payment.invoiceId && !localPaymentsByInvoiceId.has(payment.invoiceId)) { - localPaymentsByInvoiceId.set(payment.invoiceId, payment); - } - } - const expectedCustomerId = user.stripeCustomerId ?? subscription.customer_id ?? null; - const customerIds = await this.resolveCustomerIds(user); - const tryInvoice = async (invoice: BillingInvoiceRow): Promise => { - const primaryPayment = await this.billingRepository.payments.findPrimaryForInvoice(invoice.provider_id); - const chargeId = primaryPayment?.charge_id ?? null; - const paymentIntentId = primaryPayment?.payment_intent_id ?? null; - const charge = chargeId ? await this.billingRepository.charges.findById(chargeId) : null; - const checkoutSession = await this.getCheckoutSessionForInvoice({ - paymentIntentId, - subscriptionId: invoice.subscription_id, - customerIds, - }); - return this.buildRefundTargetFromMirror( - invoice, - primaryPayment, - charge, - checkoutSession, - expectedCustomerId, - localPaymentsByInvoiceId.get(invoice.provider_id) ?? null, - ); - }; - if (subscription.latest_invoice_id) { - const invoice = await this.billingRepository.invoices.findById(subscription.latest_invoice_id); - if (invoice) { - const target = await tryInvoice(invoice); - if (target) return target; - } - } - const subInvoices = await this.billingRepository.invoices.listBySubscription(subscription.provider_id, { - pageSize: 50, - }); - for (const invoice of subInvoices.rows) { - const target = await tryInvoice(invoice); - if (target) return target; - } - for (const lp of localPayments) { - if (!lp.invoiceId) continue; - const invoice = await this.billingRepository.invoices.findById(lp.invoiceId); - if (!invoice) continue; - const target = await tryInvoice(invoice); - if (target) return target; - } - return null; - } - - async applyRefundPolicyAndCancelImmediately(params: { - adminUserId: UserID; - targetUser: User; - reason?: string; - }): Promise { - if (!this.stripe || !this.stripeService) { - throw new StripePaymentNotAvailableError(); - } - const subscription = await this.resolveManageableStripeSubscription(params.targetUser); - if (!subscription) { - throw new StripeNoActiveSubscriptionError(); - } - const syncedTargetUser = await this.syncResolvedStripeState(params.targetUser, { - customerId: subscription.customer_id ?? undefined, - subscriptionId: subscription.provider_id, - premiumWillCancel: subscription.cancel_at_period_end ?? undefined, - }); - const refundTarget = await this.resolveLatestRefundableInvoiceForImmediateCancel(syncedTargetUser, subscription); - const refundDecision = decideImmediateCancelRefund({ - amountPaidCents: refundTarget?.amountPaidCents ?? 0, - now: new Date(), - refundTarget, - subscription, - }); - if (refundDecision.amountCents !== null && !refundTarget) { - throw new StripeError('No paid Stripe invoice with a refundable payment was found for this subscription'); - } - const intentId = await this.billingRepository.actionIntents.create({ - userId: BigInt(syncedTargetUser.id), - actorAdminId: BigInt(params.adminUserId), - actionType: 'cancel_and_refund', - subscriptionId: subscription.provider_id, - invoiceId: refundTarget?.invoiceId ?? null, - paymentIntentId: refundTarget?.paymentIntentId ?? null, - refundAmount: refundDecision.amountCents !== null ? BigInt(refundDecision.amountCents) : null, - refundReason: params.reason ?? null, - }); - let refund: Stripe.Response | null = null; - try { - await this.stripeService.cancelSubscriptionImmediately(syncedTargetUser.id, params.reason); - await this.billingRepository.actionIntents.markStage(intentId, 'sub_canceled', { - sub_canceled_at: new Date(), - }); - if (refundDecision.amountCents !== null && refundTarget) { - refund = await this.stripe.refunds.create( - { - ...(refundTarget.paymentIntentId - ? {payment_intent: refundTarget.paymentIntentId} - : {charge: refundTarget.chargeId!}), - amount: refundDecision.amountCents, - ...(params.reason ? {reason: 'requested_by_customer' as const} : {}), - metadata: { - intent_id: String(intentId), - admin_user_id: params.adminUserId.toString(), - target_user_id: syncedTargetUser.id.toString(), - subscription_id: subscription.provider_id, - invoice_id: refundTarget.invoiceId, - refund_policy: refundDecision.policy, - refund_policy_basis: refundDecision.basis, - eu_withdrawal_waiver_required: refundTarget.euWithdrawalWaiverRequired ? 'true' : 'false', - eu_withdrawal_waiver_accepted: refundTarget.euWithdrawalWaiverAccepted ? 'true' : 'false', - ...(refundTarget.purchaseGeoipCountryCode - ? {purchase_geoip_country_code: refundTarget.purchaseGeoipCountryCode} - : {}), - ...(refundTarget.purchaseClientCountryCode - ? {purchase_client_country_code: refundTarget.purchaseClientCountryCode} - : {}), - ...(refundTarget.euWithdrawalWaiverTextVersion - ? {eu_withdrawal_waiver_text_version: refundTarget.euWithdrawalWaiverTextVersion} - : {}), - ...(refundTarget.stripeBillingCountryCode - ? {stripe_billing_country_code: refundTarget.stripeBillingCountryCode} - : {}), - ...(refundTarget.stripePaymentMethodCountryCode - ? {stripe_payment_method_country_code: refundTarget.stripePaymentMethodCountryCode} - : {}), - ...(refundTarget.stripeCustomerCountryCode - ? {stripe_customer_country_code: refundTarget.stripeCustomerCountryCode} - : {}), - ...(refundTarget.stripeTermsOfServiceAccepted !== null - ? {stripe_terms_of_service_accepted: refundTarget.stripeTermsOfServiceAccepted ? 'true' : 'false'} - : {}), - ...(params.reason ? {admin_reason: params.reason} : {}), - }, - }, - {idempotencyKey: `admin-cancel-refund:${intentId}`}, - ); - try { - await this.billingRepository.refunds.upsertFromStripe(refund, { - invoiceId: refundTarget.invoiceId, - customerId: subscription.customer_id ?? undefined, - userId: BigInt(syncedTargetUser.id), - }); - } catch (mirrorErr) { - Logger.error( - {mirrorErr, refundId: refund.id}, - 'Mirror upsert failed after refund create; reconciler will heal', - ); - } - await this.billingRepository.actionIntents.markStage(intentId, 'refund_created', { - refund_created_at: new Date(), - refund_id: refund.id, - }); - } - await this.billingRepository.actionIntents.markStage(intentId, 'complete', { - completed_at: new Date(), - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - try { - await this.billingRepository.actionIntents.markStage(intentId, 'failed', { - error_message: message, - }); - } catch (markErr) { - Logger.error({markErr, intentId: String(intentId)}, 'Failed to mark action intent as failed'); - } - throw err; - } - const auditMetadata = new Map([ - ['subscription_id', subscription.provider_id], - ['refund_policy', refundDecision.policy], - ['refund_policy_basis', refundDecision.basis], - ['cycle_elapsed_days', String(refundDecision.cycleElapsedDays)], - ['intent_id', String(intentId)], - ]); - if (refundTarget) { - auditMetadata.set('invoice_id', refundTarget.invoiceId); - auditMetadata.set('invoice_amount_paid_cents', String(refundTarget.amountPaidCents)); - if (refundTarget.paymentIntentId) auditMetadata.set('payment_intent_id', refundTarget.paymentIntentId); - if (refundTarget.chargeId) auditMetadata.set('charge_id', refundTarget.chargeId); - } - if (refund) { - auditMetadata.set('refund_id', refund.id); - auditMetadata.set('refunded_amount_cents', String(refund.amount)); - } - if (params.reason) { - auditMetadata.set('reason', params.reason); - } - await this.auditService.createAuditLog({ - adminUserId: params.adminUserId, - targetType: 'user', - targetId: BigInt(syncedTargetUser.id), - action: 'billing_refund_policy_cancel_now', - auditLogReason: params.reason ?? null, - metadata: auditMetadata, - }); - return { - subscription_id: subscription.provider_id, - invoice_id: refundTarget?.invoiceId ?? '', - payment_intent_id: refundTarget?.paymentIntentId ?? null, - charge_id: refundTarget?.chargeId ?? null, - refund_policy: refundDecision.policy, - refund_policy_basis: refundDecision.basis, - refund_id: refund?.id ?? null, - refunded_amount_cents: refund?.amount ?? 0, - invoice_amount_paid_cents: refundTarget?.amountPaidCents ?? 0, - currency: refundTarget?.currency ?? subscription.currency ?? 'usd', - cycle_elapsed_days: refundDecision.cycleElapsedDays, - purchase_geoip_country_code: refundTarget?.purchaseGeoipCountryCode ?? null, - purchase_client_country_code: refundTarget?.purchaseClientCountryCode ?? null, - stripe_payment_method_country_code: refundTarget?.stripePaymentMethodCountryCode ?? null, - stripe_billing_country_code: refundTarget?.stripeBillingCountryCode ?? null, - stripe_customer_country_code: refundTarget?.stripeCustomerCountryCode ?? null, - stripe_terms_of_service_accepted: refundTarget?.stripeTermsOfServiceAccepted ?? null, - eu_withdrawal_waiver_required: refundTarget?.euWithdrawalWaiverRequired ?? false, - eu_withdrawal_waiver_accepted: refundTarget?.euWithdrawalWaiverAccepted ?? false, - eu_withdrawal_waiver_accepted_at: refundTarget?.euWithdrawalWaiverAcceptedAt?.toISOString() ?? null, - eu_withdrawal_waiver_text_version: refundTarget?.euWithdrawalWaiverTextVersion ?? null, - }; - } - - private async ensureManageableSubscription(targetUser: User): Promise { - const sub = await this.resolveManageableStripeSubscription(targetUser); - if (!sub) { - throw new StripeNoActiveSubscriptionError(); - } - await this.syncResolvedStripeState(targetUser, { - customerId: sub.customer_id ?? undefined, - subscriptionId: sub.provider_id, - premiumWillCancel: sub.cancel_at_period_end ?? undefined, - }); - return sub; - } - - async cancelSubscription(adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null): Promise { - if (!this.stripeService) { - throw new StripePaymentNotAvailableError(); - } - const targetUser = await getRequiredUser(this.userRepository, targetUserId); - await this.ensureManageableSubscription(targetUser); - await this.stripeService.cancelSubscriptionAtPeriodEnd(targetUserId); - await this.auditService.createAuditLog({ - adminUserId, - targetType: 'user', - targetId: BigInt(targetUserId), - action: 'billing_cancel_subscription', - auditLogReason, - }); - } - - async cancelSubscriptionImmediately( - adminUserId: UserID, - targetUserId: UserID, - auditLogReason: string | null, - ): Promise { - if (!this.stripeService) { - throw new StripePaymentNotAvailableError(); - } - const targetUser = await getRequiredUser(this.userRepository, targetUserId); - await this.ensureManageableSubscription(targetUser); - await this.stripeService.cancelSubscriptionImmediately(targetUserId, auditLogReason ?? undefined); - await this.auditService.createAuditLog({ - adminUserId, - targetType: 'user', - targetId: BigInt(targetUserId), - action: 'billing_cancel_subscription_now', - auditLogReason, - }); - } - - async endPremiumGracePeriod(adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null): Promise { - if (!this.stripeService) { - throw new StripePaymentNotAvailableError(); - } - const targetUser = await getRequiredUser(this.userRepository, targetUserId); - const wasInGrace = await this.stripeService.endPremiumGracePeriod(targetUserId); - const auditMetadata = new Map(); - auditMetadata.set('was_in_grace', String(wasInGrace)); - if (targetUser.premiumGraceEndsAt) { - auditMetadata.set('prior_grace_ends_at', targetUser.premiumGraceEndsAt.toISOString()); - } - await this.auditService.createAuditLog({ - adminUserId, - targetType: 'user', - targetId: BigInt(targetUserId), - action: 'billing_end_premium_grace_period', - auditLogReason, - metadata: auditMetadata, - }); - } - - async reactivateSubscription( - adminUserId: UserID, - targetUserId: UserID, - auditLogReason: string | null, - ): Promise { - if (!this.stripeService) { - throw new StripePaymentNotAvailableError(); - } - const targetUser = await getRequiredUser(this.userRepository, targetUserId); - await this.ensureManageableSubscription(targetUser); - await this.stripeService.reactivateSubscription(targetUserId); - await this.auditService.createAuditLog({ - adminUserId, - targetType: 'user', - targetId: BigInt(targetUserId), - action: 'billing_reactivate_subscription', - auditLogReason, - }); - } - - async getPriceForInvoice(invoice: BillingInvoiceRow): Promise { - if (!invoice.subscription_id) return null; - const sub = await this.billingRepository.subscriptions.findById(invoice.subscription_id); - if (!sub?.primary_price_id) return null; - return this.billingRepository.prices.findById(sub.primary_price_id); - } -} - -function createBillingService(ctx: {get: (key: K) => HonoEnv['Variables'][K]}): { - userRepository: IUserRepository; - service: BillingAdminControllerService; -} { - const userRepository = ctx.get('userRepository'); - const stripeService = (ctx.get('stripeService') as StripeService | undefined) ?? null; - const snowflakeService = ctx.get('snowflakeService') as ISnowflakeService; - const auditService = new AdminAuditService(new AdminRepository(), snowflakeService); - const billingRepository = getBillingRepository(); - return { - userRepository, - service: new BillingAdminControllerService(userRepository, stripeService, auditService, billingRepository), - }; -} - -function mapMirrorPaymentRecordToResponse(user: User, record: MirrorBillingPaymentRecord) { - const checkoutSessionMetadata = getCheckoutSessionMetadata(record.checkoutSession); - const localPayment = record.localPayment; - const resolvedPaymentIntentId = record.primaryPayment?.payment_intent_id ?? null; - const refundedAmountCents = getRefundAmount(record.refunds); - const stripePaymentMethodCountryCode = normalizeCountryCode(record.charge?.card_country ?? null); - const stripeBillingCountryCode: string | null = null; - const stripeCustomerCountryCode: string | null = null; - const stripeTermsOfServiceAccepted: boolean | null = null; - const metadataWaiverRequired = parseStripeMetadataBoolean( - getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_required'), - ); - const metadataWaiverAccepted = parseStripeMetadataBoolean( - getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_accepted'), - ); - const stripeCountryRequiresWaiver = [ - stripeBillingCountryCode, - stripeCustomerCountryCode, - stripePaymentMethodCountryCode, - ].some((countryCode) => isEuEeaCountryCode(countryCode)); - const euWithdrawalWaiverRequired = - (localPayment?.euWithdrawalWaiverRequired ?? false) || - metadataWaiverRequired === true || - stripeCountryRequiresWaiver; - const euWithdrawalWaiverAccepted = - (localPayment?.euWithdrawalWaiverAccepted ?? false) || metadataWaiverAccepted === true; - const amountPaid = Number(record.invoice.amount_paid ?? 0n); - return { - checkout_session_id: record.checkoutSession?.provider_id ?? null, - user_id: user.id.toString(), - stripe_customer_id: record.invoice.customer_id ?? null, - payment_intent_id: resolvedPaymentIntentId, - resolved_payment_intent_id: resolvedPaymentIntentId, - charge_id: record.primaryPayment?.charge_id ?? null, - subscription_id: record.invoice.subscription_id ?? null, - invoice_id: record.invoice.provider_id, - price_id: null, - product_type: getMetadataString(checkoutSessionMetadata, 'product_type') ?? record.invoice.billing_reason ?? null, - amount_cents: amountPaid, - currency: record.invoice.currency ?? 'usd', - status: buildPaymentStatus(record.invoice, record.primaryPayment, record.refunds), - stripe_source: 'invoice' as const, - refundable_via_payment_intent: resolvedPaymentIntentId !== null && refundedAmountCents < amountPaid, - refunded_amount_cents: refundedAmountCents, - net_amount_cents: Math.max(0, amountPaid - refundedAmountCents), - refunds: record.refunds.map((refund) => ({ - id: refund.provider_id, - amount_cents: Number(refund.amount ?? 0n), - currency: refund.currency ?? 'usd', - status: refund.status ?? null, - reason: refund.reason ?? null, - created: refund.stripe_created_at ? Math.floor(refund.stripe_created_at.getTime() / 1000) : 0, - payment_intent_id: refund.payment_intent_id ?? null, - charge_id: refund.charge_id ?? null, - })), - payment_method_type: record.charge?.payment_method_type ?? null, - payment_method_brand: record.charge?.card_brand ?? null, - payment_method_last4: record.charge?.card_last4 ?? null, - stripe_payment_method_country_code: stripePaymentMethodCountryCode, - stripe_billing_country_code: stripeBillingCountryCode, - stripe_customer_country_code: stripeCustomerCountryCode, - stripe_terms_of_service_accepted: stripeTermsOfServiceAccepted, - is_gift: getMetadataString(checkoutSessionMetadata, 'is_gift') === 'true', - gift_code: getMetadataString(checkoutSessionMetadata, 'gift_code'), - purchase_geoip_country_code: normalizeCountryCode( - getMetadataString(checkoutSessionMetadata, 'purchase_geoip_country_code'), - ), - purchase_client_country_code: normalizeCountryCode( - getMetadataString(checkoutSessionMetadata, 'purchase_client_country_code'), - ), - eu_withdrawal_waiver_required: euWithdrawalWaiverRequired, - eu_withdrawal_waiver_accepted: euWithdrawalWaiverAccepted, - eu_withdrawal_waiver_accepted_at: getCheckoutSessionAcceptedAt(record.checkoutSession)?.toISOString() ?? null, - eu_withdrawal_waiver_text_version: getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_text_version'), - created_at: (record.invoice.stripe_created_at ?? new Date(0)).toISOString(), - completed_at: - record.primaryPayment?.paid_at?.toISOString() ?? - (record.invoice.status === 'paid' - ? ((record.invoice.paid_at ?? record.invoice.stripe_created_at ?? null)?.toISOString() ?? null) - : null), - }; -} - -function mapInvoiceRowToResponse(invoice: BillingInvoiceRow, primaryPayment: BillingPaymentRow | null) { - return { - id: invoice.provider_id, - amount_due: Number(invoice.amount_due ?? 0n), - amount_paid: Number(invoice.amount_paid ?? 0n), - currency: invoice.currency ?? 'usd', - status: invoice.status ?? null, - created: invoice.stripe_created_at ? Math.floor(invoice.stripe_created_at.getTime() / 1000) : 0, - billing_reason: invoice.billing_reason ?? null, - subscription_id: invoice.subscription_id ?? null, - payment_type: null, - payment_status: primaryPayment?.status ?? null, - payment_intent_id: primaryPayment?.payment_intent_id ?? null, - charge_id: primaryPayment?.charge_id ?? null, - paid_at: (primaryPayment?.paid_at ?? invoice.paid_at)?.toISOString() ?? null, - hosted_invoice_url: invoice.hosted_invoice_url ?? null, - invoice_pdf: invoice.invoice_pdf ?? null, - }; -} - -function mapSubscriptionRowToResponse(sub: BillingSubscriptionRow, primaryPrice: BillingPriceRow | null) { - return { - id: sub.provider_id, - status: sub.status ?? 'unknown', - current_period_start: sub.current_period_start?.toISOString() ?? null, - current_period_end: sub.current_period_end?.toISOString() ?? null, - cancel_at_period_end: sub.cancel_at_period_end ?? false, - cancel_at: sub.cancel_at?.toISOString() ?? null, - canceled_at: sub.canceled_at?.toISOString() ?? null, - plan_interval: primaryPrice?.interval ?? null, - plan_amount_cents: - primaryPrice?.unit_amount !== null && primaryPrice?.unit_amount !== undefined - ? Number(primaryPrice.unit_amount) - : null, - plan_currency: primaryPrice?.currency ?? sub.currency ?? null, - default_payment_method_id: sub.default_payment_method ?? null, - }; -} - -function mapPaymentMethodRowToResponse(pm: BillingPaymentMethodRow) { - return { - id: pm.provider_id, - type: pm.type ?? 'card', - card_brand: pm.card_brand ?? null, - card_last4: pm.card_last4 ?? null, - card_exp_month: pm.card_exp_month ?? null, - card_exp_year: pm.card_exp_year ?? null, - created: pm.stripe_created_at ? Math.floor(pm.stripe_created_at.getTime() / 1000) : 0, - }; -} - -function buildEmptySubscriptionResponse() { - return { - id: '', - status: 'none', - current_period_start: null, - current_period_end: null, - cancel_at_period_end: false, - cancel_at: null, - canceled_at: null, - plan_interval: null, - plan_amount_cents: null, - plan_currency: null, - default_payment_method_id: null, - }; -} - -function clampInvoiceLimit(rawLimit: string | undefined): number { - if (!rawLimit) return 25; - const parsed = parseInt(rawLimit, 10); - if (!Number.isFinite(parsed)) return 25; - return Math.max(1, Math.min(parsed, 100)); -} - -export function BillingAdminController(app: HonoApp) { - app.get( - '/admin/billing/users/:userId/overview', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_overview', - summary: 'Get billing overview for a user', - description: 'Retrieve subscription status, payment history, and Stripe payment methods for a user.', - responseSchema: AdminBillingOverviewResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const [payments, subscription, paymentMethods, stripeCustomerId] = await Promise.all([ - billingService.getMirrorBillingPaymentRecords(user), - billingService.getUserSubscription(user), - billingService.getUserPaymentMethods(user), - billingService.getResolvedStripeCustomerId(user), - ]); - const subscriptionPrice = subscription - ? await getBillingRepository().prices.findById(subscription.primary_price_id ?? '') - : null; - return ctx.json({ - subscription: subscription ? mapSubscriptionRowToResponse(subscription, subscriptionPrice) : null, - payments: payments.map((payment) => mapMirrorPaymentRecordToResponse(user, payment)), - payment_methods: paymentMethods.map(mapPaymentMethodRowToResponse), - stripe_customer_id: stripeCustomerId, - }); - }, - ); - app.get( - '/admin/billing/guilds/:guildId/overview', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_guild_overview', - summary: 'Get billing overview for a guild', - description: - 'Retrieve guild billing state. The current billing mirror is user-scoped, so guilds without persisted billing records return an empty overview.', - responseSchema: AdminBillingOverviewResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const guildId = ctx.req.param('guildId'); - const adminService = ctx.get('adminService'); - const lookup = await adminService.guildServiceAggregate.lookupService.lookupGuild({guild_id: BigInt(guildId)}); - if (!lookup.guild) { - throw new UnknownGuildError(); - } - return ctx.json({ - subscription: null, - payments: [], - payment_methods: [], - stripe_customer_id: null, - }); - }, - ); - app.get( - '/admin/billing/users/:userId/payments', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_list_payments', - summary: 'List payments for a user', - description: 'Retrieve the payment history stored for a user.', - responseSchema: AdminPaymentListResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const payments = await billingService.getMirrorBillingPaymentRecords(user); - return ctx.json({ - payments: payments.map((payment) => mapMirrorPaymentRecordToResponse(user, payment)), - }); - }, - ); - app.get( - '/admin/billing/users/:userId/subscription', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_get_subscription', - summary: 'Get subscription for a user', - description: 'Retrieve the current Stripe subscription details for a user.', - responseSchema: AdminSubscriptionResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const subscription = await billingService.getUserSubscription(user); - const subscriptionPrice = subscription?.primary_price_id - ? await getBillingRepository().prices.findById(subscription.primary_price_id) - : null; - return ctx.json( - subscription ? mapSubscriptionRowToResponse(subscription, subscriptionPrice) : buildEmptySubscriptionResponse(), - ); - }, - ); - app.get( - '/admin/billing/users/:userId/payment-methods', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_list_payment_methods', - summary: 'List payment methods for a user', - description: 'Retrieve the Stripe payment methods associated with a user.', - responseSchema: AdminPaymentMethodListResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const paymentMethods = await billingService.getUserPaymentMethods(user); - return ctx.json({ - payment_methods: paymentMethods.map(mapPaymentMethodRowToResponse), - }); - }, - ); - app.get( - '/admin/billing/users/:userId/invoices', - requireAdminACL(AdminACLs.BILLING_VIEW), - OpenAPI({ - operationId: 'admin_billing_list_invoices', - summary: 'List invoices for a user', - description: 'Retrieve recent Stripe invoices for a user.', - responseSchema: AdminInvoiceListResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const limit = clampInvoiceLimit(ctx.req.query('limit') ?? undefined); - const result = await billingService.getUserInvoices(user, limit); - const billingRepo = getBillingRepository(); - const invoiceResponses = await Promise.all( - result.invoices.map(async (invoice) => { - const primaryPayment = await billingRepo.payments.findPrimaryForInvoice(invoice.provider_id); - return mapInvoiceRowToResponse(invoice, primaryPayment); - }), - ); - return ctx.json({ - invoices: invoiceResponses, - has_more: result.has_more, - }); - }, - ); - app.post( - '/admin/billing/users/:userId/refund', - requireAdminACL(AdminACLs.BILLING_REFUND), - Validator('json', AdminBillingRefundRequest), - OpenAPI({ - operationId: 'admin_billing_refund', - summary: 'Issue a refund for a user payment', - description: 'Issue a full or partial refund for a user payment through Stripe.', - responseSchema: null, - statusCode: 204, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const {payment_intent_id, amount_cents, reason} = ctx.req.valid('json'); - await billingService.issueRefund({ - adminUserId: ctx.get('adminUserId'), - targetUser: user, - paymentIntentId: payment_intent_id, - amountCents: amount_cents, - reason: reason ?? undefined, - }); - return ctx.body(null, 204); - }, - ); - app.post( - '/admin/billing/users/:userId/refund-policy-cancel-now', - requireAdminACL(AdminACLs.BILLING_REFUND), - requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), - Validator('json', AdminBillingRefundLatestInvoiceCancelRequest), - OpenAPI({ - operationId: 'admin_billing_refund_policy_cancel_now', - summary: 'Apply refund policy and cancel subscription immediately', - description: - 'Cancels a user subscription immediately and applies the support refund policy against the latest paid Stripe invoice.', - responseSchema: AdminBillingRefundLatestInvoiceCancelResponse, - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {userRepository, service: billingService} = createBillingService(ctx); - const user = await getRequiredUser(userRepository, userId); - const {reason} = ctx.req.valid('json'); - const result = await billingService.applyRefundPolicyAndCancelImmediately({ - adminUserId: ctx.get('adminUserId'), - targetUser: user, - reason: reason ?? undefined, - }); - return ctx.json(result); - }, - ); - app.post( - '/admin/billing/users/:userId/cancel-subscription-now', - requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), - Validator('json', AdminBillingCancelImmediatelyRequest), - OpenAPI({ - operationId: 'admin_billing_cancel_subscription_now', - summary: 'Cancel a user subscription immediately', - description: 'Cancel a user Stripe subscription immediately without issuing a refund.', - responseSchema: null, - statusCode: 204, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {service: billingService} = createBillingService(ctx); - const {reason} = ctx.req.valid('json'); - await billingService.cancelSubscriptionImmediately(ctx.get('adminUserId'), userId, reason ?? null); - return ctx.body(null, 204); - }, - ); - app.post( - '/admin/billing/users/:userId/cancel-subscription', - requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), - OpenAPI({ - operationId: 'admin_billing_cancel_subscription', - summary: 'Cancel a user subscription', - description: 'Set a user Stripe subscription to cancel at period end.', - responseSchema: null, - statusCode: 204, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {service: billingService} = createBillingService(ctx); - await billingService.cancelSubscription(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); - return ctx.body(null, 204); - }, - ); - app.post( - '/admin/billing/users/:userId/end-premium-grace-period', - requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), - OpenAPI({ - operationId: 'admin_billing_end_premium_grace_period', - summary: "End a user's premium grace period", - description: - 'End the post-cancel premium grace period for a user immediately, downgrading them and clearing premium_since. Idempotent: safe to call when not in grace. Use when investigating fraud or honoring a user request to opt out of the recovery window.', - responseSchema: null, - statusCode: 204, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {service: billingService} = createBillingService(ctx); - await billingService.endPremiumGracePeriod(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); - return ctx.body(null, 204); - }, - ); - app.post( - '/admin/billing/users/:userId/reactivate-subscription', - requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), - OpenAPI({ - operationId: 'admin_billing_reactivate_subscription', - summary: 'Reactivate a user subscription', - description: 'Remove a period-end cancellation from a user Stripe subscription.', - responseSchema: null, - statusCode: 204, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - ensureBillingFeatureAvailable(); - const userId = createUserID(BigInt(ctx.req.param('userId'))); - const {service: billingService} = createBillingService(ctx); - await billingService.reactivateSubscription(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); - return ctx.body(null, 204); - }, - ); -} diff --git a/fluxer_api/src/api/admin/controllers/index.ts b/fluxer_api/src/api/admin/controllers/index.ts index eafb37d20..8c558a3c8 100644 --- a/fluxer_api/src/api/admin/controllers/index.ts +++ b/fluxer_api/src/api/admin/controllers/index.ts @@ -7,7 +7,6 @@ import {ArchiveAdminController} from './ArchiveAdminController'; import {AssetAdminController} from './AssetAdminController'; import {AuditLogAdminController} from './AuditLogAdminController'; import {BanAdminController} from './BanAdminController'; -import {BillingAdminController} from './BillingAdminController'; import {BulkAdminController} from './BulkAdminController'; import {CodesAdminController} from './CodesAdminController'; import {DiscoveryAdminController} from './DiscoveryAdminController'; @@ -39,7 +38,6 @@ export function registerAdminControllers(app: HonoApp) { AuditLogAdminController(app); ArchiveAdminController(app); ReportAdminController(app); - BillingAdminController(app); VoiceAdminController(app); GatewayAdminController(app); SearchAdminController(app); diff --git a/fluxer_api/src/api/admin/tests/AdminBillingAuthorization.test.ts b/fluxer_api/src/api/admin/tests/AdminBillingAuthorization.test.ts deleted file mode 100644 index d9901f894..000000000 --- a/fluxer_api/src/api/admin/tests/AdminBillingAuthorization.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeEach, describe, test} from 'vitest'; -import {createTestAccount, setUserACLs} from '../../auth/tests/AuthTestUtils'; -import type {ApiTestHarness} from '../../test/ApiTestHarness'; -import {createApiTestHarness} from '../../test/ApiTestHarness'; -import {HTTP_STATUS} from '../../test/TestConstants'; -import {createBuilder} from '../../test/TestRequestBuilder'; - -describe('Admin Billing Authorization', () => { - let harness: ApiTestHarness; - beforeEach(async () => { - harness = await createApiTestHarness(); - }); - test('billing overview requires billing:view ACL', async () => { - const admin = await createTestAccount(harness); - await setUserACLs(harness, admin, ['admin:authenticate']); - await createBuilder(harness, `${admin.token}`) - .get(`/admin/billing/users/${admin.userId}/overview`) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('billing refund requires billing:refund ACL', async () => { - const admin = await createTestAccount(harness); - await setUserACLs(harness, admin, ['admin:authenticate']); - await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${admin.userId}/refund`) - .body({payment_intent_id: 'pi_test_refund'}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('billing subscription management requires billing:manage_subscription ACL', async () => { - const admin = await createTestAccount(harness); - await setUserACLs(harness, admin, ['admin:authenticate']); - await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${admin.userId}/cancel-subscription`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${admin.userId}/reactivate-subscription`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('refund policy immediate cancellation requires both refund and subscription management ACLs', async () => { - const refundOnlyAdmin = await createTestAccount(harness); - await setUserACLs(harness, refundOnlyAdmin, ['admin:authenticate', 'billing:refund']); - await createBuilder(harness, `${refundOnlyAdmin.token}`) - .post(`/admin/billing/users/${refundOnlyAdmin.userId}/refund-policy-cancel-now`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - const subscriptionOnlyAdmin = await createTestAccount(harness); - await setUserACLs(harness, subscriptionOnlyAdmin, ['admin:authenticate', 'billing:manage_subscription']); - await createBuilder(harness, `${subscriptionOnlyAdmin.token}`) - .post(`/admin/billing/users/${subscriptionOnlyAdmin.userId}/refund-policy-cancel-now`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); -}); diff --git a/fluxer_api/src/api/admin/tests/AdminBillingOverview.test.ts b/fluxer_api/src/api/admin/tests/AdminBillingOverview.test.ts deleted file mode 100644 index 342ec84ff..000000000 --- a/fluxer_api/src/api/admin/tests/AdminBillingOverview.test.ts +++ /dev/null @@ -1,924 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; -import type { - AdminBillingOverviewResponse, - AdminBillingRefundLatestInvoiceCancelResponse, - AdminInvoiceListResponse, -} from '@fluxer/schema/src/domains/admin/AdminBillingSchemas'; -import type Stripe from 'stripe'; -import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest'; -import {createTestAccount, setUserACLs} from '../../auth/tests/AuthTestUtils'; -import {createUserID} from '../../BrandedTypes'; -import {getBillingRepository} from '../../middleware/ServiceRegistry'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import {createStripeApiHandlers} from '../../test/msw/handlers/StripeApiHandlers'; -import {server} from '../../test/msw/server'; -import {createBuilder} from '../../test/TestRequestBuilder'; -import {PaymentRepository} from '../../user/repositories/PaymentRepository'; - -const DAY_SECONDS = 24 * 60 * 60; - -function stripeFixture(value: object): T { - return value as T; -} - -describe('Admin billing overview', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - afterAll(async () => { - await harness.shutdown(); - }); - beforeEach(async () => { - await harness.reset(); - }); - afterEach(() => { - server.resetHandlers(); - }); - async function setStripeCustomerId(userId: string, stripeCustomerId: string): Promise { - await createBuilder(harness, '') - .post(`/test/users/${userId}/premium`) - .body({stripe_customer_id: stripeCustomerId}) - .execute(); - } - async function mirrorCustomer(params: {stripeCustomerId: string; userId?: string}): Promise { - await getBillingRepository().customers.upsertFromStripe( - { - id: params.stripeCustomerId, - object: 'customer', - created: Math.floor(Date.now() / 1000), - email: null, - invoice_settings: {default_payment_method: null}, - livemode: false, - metadata: params.userId ? {userId: params.userId} : {}, - } as Stripe.Customer, - params.userId ? {knownUserId: BigInt(params.userId)} : undefined, - ); - } - async function mirrorSubscription(params: { - currentPeriodEnd?: number; - currentPeriodStart?: number; - latestInvoiceId?: string; - status?: Stripe.Subscription.Status; - stripeCustomerId: string; - stripeSubscriptionId: string; - userId?: string; - }): Promise { - const now = Math.floor(Date.now() / 1000); - const currentPeriodStart = params.currentPeriodStart ?? now - DAY_SECONDS; - const currentPeriodEnd = params.currentPeriodEnd ?? now + 29 * DAY_SECONDS; - await getBillingRepository().subscriptions.upsertFromStripe( - { - id: params.stripeSubscriptionId, - cancel_at: null, - cancel_at_period_end: false, - canceled_at: null, - collection_method: 'charge_automatically', - created: currentPeriodStart, - currency: 'eur', - customer: params.stripeCustomerId, - items: { - data: [ - { - id: `si_${params.stripeSubscriptionId}`, - current_period_start: currentPeriodStart, - current_period_end: currentPeriodEnd, - price: { - id: 'price_monthly_eur', - product: 'prod_monthly', - unit_amount: 499, - }, - quantity: 1, - }, - ], - }, - latest_invoice: params.latestInvoiceId ?? null, - livemode: false, - metadata: params.userId ? {userId: params.userId} : {}, - status: params.status ?? 'active', - }, - params.userId ? {knownUserId: BigInt(params.userId)} : undefined, - ); - } - async function mirrorInvoice(params: { - amountPaidCents: number; - chargeId?: string; - created?: number; - currency?: string; - invoiceId: string; - paymentIntentId?: string; - paymentId?: string; - stripeCustomerId: string; - stripeSubscriptionId?: string; - userId?: string; - }): Promise { - const created = params.created ?? Math.floor(Date.now() / 1000); - await getBillingRepository().invoices.upsertFromStripe( - stripeFixture({ - id: params.invoiceId, - object: 'invoice', - amount_due: params.amountPaidCents, - amount_paid: params.amountPaidCents, - amount_remaining: 0, - attempt_count: 1, - attempted: true, - billing_reason: 'subscription_cycle', - collection_method: 'charge_automatically', - created, - currency: params.currency ?? 'eur', - customer: params.stripeCustomerId, - livemode: false, - metadata: params.userId ? {userId: params.userId} : {}, - paid: true, - payments: - params.paymentIntentId || params.chargeId - ? { - object: 'list', - data: [ - { - id: params.paymentId ?? `inpay_${params.invoiceId}`, - object: 'invoice_payment', - amount_paid: params.amountPaidCents, - amount_requested: params.amountPaidCents, - created, - currency: params.currency ?? 'eur', - invoice: params.invoiceId, - is_default: true, - livemode: false, - payment: { - type: 'payment_intent', - payment_intent: params.paymentIntentId ?? null, - charge: params.chargeId ?? null, - }, - status: 'paid', - status_transitions: {canceled_at: null, paid_at: created + 20}, - }, - ], - has_more: false, - url: `/v1/invoices/${params.invoiceId}/payments`, - } - : {object: 'list', data: [], has_more: false, url: `/v1/invoices/${params.invoiceId}/payments`}, - status: 'paid', - status_transitions: {finalized_at: created, paid_at: created + 20, voided_at: null}, - subscription: params.stripeSubscriptionId ?? null, - subtotal: params.amountPaidCents, - total: params.amountPaidCents, - }), - params.userId ? {knownUserId: BigInt(params.userId)} : undefined, - ); - } - async function mirrorPaymentIntent(params: { - amountCents?: number; - chargeId?: string; - invoiceId?: string; - paymentIntentId: string; - stripeCustomerId: string; - }): Promise { - await getBillingRepository().paymentIntents.upsertFromStripe( - stripeFixture({ - id: params.paymentIntentId, - object: 'payment_intent', - amount: params.amountCents ?? 499, - amount_capturable: 0, - amount_received: params.amountCents ?? 499, - capture_method: 'automatic', - confirmation_method: 'automatic', - created: Math.floor(Date.now() / 1000), - currency: 'eur', - customer: params.stripeCustomerId, - invoice: params.invoiceId ?? null, - latest_charge: params.chargeId ?? null, - livemode: false, - metadata: {}, - payment_method_types: ['card'], - status: 'succeeded', - }), - ); - } - async function mirrorCharge(params: { - amountCents?: number; - chargeId: string; - invoiceId?: string; - paymentIntentId?: string; - stripeCustomerId: string; - }): Promise { - await getBillingRepository().charges.upsertFromStripe( - stripeFixture({ - id: params.chargeId, - object: 'charge', - amount: params.amountCents ?? 499, - amount_captured: params.amountCents ?? 499, - amount_refunded: 0, - billing_details: {address: {country: null}}, - captured: true, - created: Math.floor(Date.now() / 1000), - currency: 'eur', - customer: params.stripeCustomerId, - invoice: params.invoiceId ?? null, - livemode: false, - metadata: {}, - paid: true, - payment_intent: params.paymentIntentId ?? null, - payment_method_details: {type: 'card', card: {brand: 'visa', last4: '4242', country: null}}, - refunded: false, - status: 'succeeded', - }), - ); - } - async function mirrorPaymentMethod(params: {paymentMethodId: string; stripeCustomerId: string}): Promise { - await getBillingRepository().paymentMethods.upsertFromStripe( - { - id: params.paymentMethodId, - object: 'payment_method', - billing_details: {address: {country: 'US'}, email: null, name: null, phone: null}, - card: {brand: 'visa', country: 'US', exp_month: 12, exp_year: 2031, funding: 'credit', last4: '4242'}, - created: Math.floor(Date.now() / 1000), - customer: params.stripeCustomerId, - livemode: false, - metadata: {}, - type: 'card', - } as Stripe.PaymentMethod, - {isDefault: true}, - ); - } - async function setStripeSubscriptionState(params: { - userId: string; - stripeCustomerId: string; - stripeSubscriptionId: string; - }): Promise { - await createBuilder(harness, '') - .post(`/test/users/${params.userId}/premium`) - .body({ - stripe_customer_id: params.stripeCustomerId, - stripe_subscription_id: params.stripeSubscriptionId, - premium_type: UserPremiumTypes.SUBSCRIPTION, - premium_billing_cycle: 'monthly', - premium_will_cancel: false, - }) - .execute(); - } - function createRefundPolicyStripeHandlers(params: { - amountPaidCents: number; - currency?: string; - elapsedDays: number; - invoiceId: string; - stripeCustomerId: string; - stripeSubscriptionId: string; - }) { - const now = Math.floor(Date.now() / 1000); - const currentPeriodStart = now - params.elapsedDays * DAY_SECONDS; - const currentPeriodEnd = currentPeriodStart + 30 * DAY_SECONDS; - return createStripeApiHandlers({ - subscriptions: { - [params.stripeSubscriptionId]: { - customer: params.stripeCustomerId, - current_period_start: currentPeriodStart, - current_period_end: currentPeriodEnd, - latest_invoice: params.invoiceId, - status: 'active', - }, - }, - invoices: { - [params.invoiceId]: { - customer: params.stripeCustomerId, - subscriptionId: params.stripeSubscriptionId, - amount_due: params.amountPaidCents, - amount_paid: params.amountPaidCents, - billing_reason: 'subscription_cycle', - currency: params.currency ?? 'eur', - created: now - 300, - status: 'paid', - }, - }, - }); - } - async function createPaymentRecord(params: { - userId: string; - checkoutSessionId: string; - completedAt?: Date; - euWithdrawalWaiverAccepted?: boolean; - euWithdrawalWaiverAcceptedAt?: Date | null; - euWithdrawalWaiverRequired?: boolean; - euWithdrawalWaiverTextVersion?: string | null; - invoiceId: string; - purchaseClientCountryCode?: string | null; - purchaseGeoipCountryCode?: string | null; - subscriptionId: string; - stripeCustomerId: string; - }): Promise { - const paymentRepository = new PaymentRepository(); - const createdAt = params.completedAt ?? new Date('2026-02-23T14:27:32.409Z'); - await paymentRepository.createPayment({ - checkout_session_id: params.checkoutSessionId, - user_id: createUserID(BigInt(params.userId)), - price_id: 'price_monthly_eur', - product_type: 'monthly_subscription', - status: 'completed', - is_gift: false, - created_at: createdAt, - purchase_geoip_country_code: params.purchaseGeoipCountryCode ?? null, - purchase_client_country_code: params.purchaseClientCountryCode ?? null, - eu_withdrawal_waiver_required: params.euWithdrawalWaiverRequired ?? false, - eu_withdrawal_waiver_accepted: params.euWithdrawalWaiverAccepted ?? false, - eu_withdrawal_waiver_accepted_at: params.euWithdrawalWaiverAcceptedAt ?? null, - eu_withdrawal_waiver_text_version: params.euWithdrawalWaiverTextVersion ?? null, - }); - await paymentRepository.updatePayment({ - checkout_session_id: params.checkoutSessionId, - stripe_customer_id: params.stripeCustomerId, - payment_intent_id: null, - subscription_id: params.subscriptionId, - invoice_id: params.invoiceId, - amount_cents: 499, - currency: 'eur', - status: 'completed', - completed_at: createdAt, - purchase_geoip_country_code: params.purchaseGeoipCountryCode ?? null, - purchase_client_country_code: params.purchaseClientCountryCode ?? null, - eu_withdrawal_waiver_required: params.euWithdrawalWaiverRequired ?? false, - eu_withdrawal_waiver_accepted: params.euWithdrawalWaiverAccepted ?? false, - eu_withdrawal_waiver_accepted_at: params.euWithdrawalWaiverAcceptedAt ?? null, - eu_withdrawal_waiver_text_version: params.euWithdrawalWaiverTextVersion ?? null, - }); - } - test('resolves missing payment intents from Stripe invoice payments for overview and invoice listings', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), ['admin:authenticate', 'billing:view']); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_billing_target'; - await setStripeCustomerId(targetUser.userId, stripeCustomerId); - await createPaymentRecord({ - userId: targetUser.userId, - checkoutSessionId: 'cs_billing_overview_1', - invoiceId: 'in_local_checkout_1', - subscriptionId: 'sub_billing_target', - stripeCustomerId, - }); - const stripeHandlers = createStripeApiHandlers({ - invoices: { - in_local_checkout_1: { - customer: stripeCustomerId, - subscriptionId: 'sub_billing_target', - amount_due: 499, - amount_paid: 499, - billing_reason: 'subscription_create', - currency: 'eur', - created: 1771862851, - payments: { - object: 'list', - data: [ - { - id: 'inpay_local_checkout_1', - object: 'invoice_payment', - amount_paid: 499, - amount_requested: 499, - created: 1771862851, - currency: 'eur', - invoice: 'in_local_checkout_1', - is_default: true, - livemode: false, - payment: { - type: 'payment_intent', - payment_intent: 'pi_local_checkout_1', - charge: 'ch_local_checkout_1', - }, - status: 'paid', - status_transitions: { - canceled_at: null, - paid_at: 1771862871, - }, - }, - ], - has_more: false, - url: '/v1/invoices/in_local_checkout_1/payments', - }, - }, - in_renewal_1: { - customer: stripeCustomerId, - subscriptionId: 'sub_billing_target', - amount_due: 499, - amount_paid: 499, - billing_reason: 'subscription_cycle', - currency: 'eur', - created: 1776065330, - payments: { - object: 'list', - data: [ - { - id: 'inpay_renewal_1', - object: 'invoice_payment', - amount_paid: 499, - amount_requested: 499, - created: 1776065330, - currency: 'eur', - invoice: 'in_renewal_1', - is_default: true, - livemode: false, - payment: { - type: 'payment_intent', - payment_intent: 'pi_renewal_1', - charge: 'ch_renewal_1', - }, - status: 'paid', - status_transitions: { - canceled_at: null, - paid_at: 1776065360, - }, - }, - ], - has_more: false, - url: '/v1/invoices/in_renewal_1/payments', - }, - }, - }, - paymentIntents: { - pi_local_checkout_1: { - customer: stripeCustomerId, - currency: 'eur', - latest_charge: 'ch_local_checkout_1', - }, - pi_renewal_1: { - customer: stripeCustomerId, - currency: 'eur', - latest_charge: 'ch_renewal_1', - }, - }, - paymentMethods: { - pm_billing_target_1: { - customer: stripeCustomerId, - type: 'card', - card: { - brand: 'visa', - last4: '4242', - exp_month: 12, - exp_year: 2031, - country: 'US', - }, - }, - }, - }); - server.use(...stripeHandlers.handlers); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: 'ch_local_checkout_1', - created: 1771862851, - invoiceId: 'in_local_checkout_1', - paymentId: 'inpay_local_checkout_1', - paymentIntentId: 'pi_local_checkout_1', - stripeCustomerId, - stripeSubscriptionId: 'sub_billing_target', - userId: targetUser.userId, - }); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: 'ch_renewal_1', - created: 1776065330, - invoiceId: 'in_renewal_1', - paymentId: 'inpay_renewal_1', - paymentIntentId: 'pi_renewal_1', - stripeCustomerId, - stripeSubscriptionId: 'sub_billing_target', - userId: targetUser.userId, - }); - await mirrorPaymentIntent({ - chargeId: 'ch_local_checkout_1', - invoiceId: 'in_local_checkout_1', - paymentIntentId: 'pi_local_checkout_1', - stripeCustomerId, - }); - await mirrorPaymentIntent({ - chargeId: 'ch_renewal_1', - invoiceId: 'in_renewal_1', - paymentIntentId: 'pi_renewal_1', - stripeCustomerId, - }); - await mirrorCharge({ - chargeId: 'ch_local_checkout_1', - invoiceId: 'in_local_checkout_1', - paymentIntentId: 'pi_local_checkout_1', - stripeCustomerId, - }); - await mirrorCharge({ - chargeId: 'ch_renewal_1', - invoiceId: 'in_renewal_1', - paymentIntentId: 'pi_renewal_1', - stripeCustomerId, - }); - await mirrorPaymentMethod({paymentMethodId: 'pm_billing_target_1', stripeCustomerId}); - const overview = await createBuilder(harness, `${admin.token}`) - .get(`/admin/billing/users/${targetUser.userId}/overview`) - .execute(); - expect(overview.payments).toHaveLength(2); - const localCheckoutPayment = overview.payments.find((payment) => payment.invoice_id === 'in_local_checkout_1'); - expect(localCheckoutPayment?.payment_intent_id).toBe('pi_local_checkout_1'); - expect(localCheckoutPayment?.resolved_payment_intent_id).toBe('pi_local_checkout_1'); - expect(localCheckoutPayment?.charge_id).toBe('ch_local_checkout_1'); - expect(localCheckoutPayment?.refundable_via_payment_intent).toBe(true); - expect(overview.payment_methods[0]?.id).toBe('pm_billing_target_1'); - const invoices = await createBuilder(harness, `${admin.token}`) - .get(`/admin/billing/users/${targetUser.userId}/invoices`) - .execute(); - expect(invoices.invoices).toHaveLength(2); - expect(invoices.invoices[0]?.id).toBe('in_renewal_1'); - expect(invoices.invoices[0]?.payment_intent_id).toBe('pi_renewal_1'); - expect(invoices.invoices[0]?.charge_id).toBe('ch_renewal_1'); - expect(invoices.invoices[0]?.billing_reason).toBe('subscription_cycle'); - expect(invoices.invoices[1]?.payment_intent_id).toBe('pi_local_checkout_1'); - }); - test('resolves billing overview from Stripe metadata even when local Stripe linkage is missing', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), ['admin:authenticate', 'billing:view']); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_billing_metadata_only'; - const stripeSubscriptionId = 'sub_billing_metadata_only'; - const invoiceId = 'in_billing_metadata_only'; - const now = Math.floor(Date.now() / 1000); - const stripeHandlers = createStripeApiHandlers({ - customers: { - [stripeCustomerId]: { - email: 'target@example.com', - metadata: { - userId: targetUser.userId, - }, - }, - }, - invoices: { - [invoiceId]: { - customer: stripeCustomerId, - subscriptionId: stripeSubscriptionId, - amount_due: 499, - amount_paid: 499, - billing_reason: 'subscription_create', - currency: 'eur', - created: now - 300, - status: 'paid', - }, - }, - paymentMethods: { - pm_billing_metadata_only: { - customer: stripeCustomerId, - type: 'card', - card: { - brand: 'visa', - last4: '1111', - exp_month: 8, - exp_year: 2031, - country: 'FR', - }, - }, - }, - subscriptions: { - [stripeSubscriptionId]: { - customer: stripeCustomerId, - latest_invoice: invoiceId, - status: 'active', - current_period_start: now - DAY_SECONDS, - current_period_end: now + 29 * DAY_SECONDS, - }, - }, - }); - server.use(...stripeHandlers.handlers); - await mirrorCustomer({stripeCustomerId, userId: targetUser.userId}); - await mirrorSubscription({ - currentPeriodEnd: now + 29 * DAY_SECONDS, - currentPeriodStart: now - DAY_SECONDS, - latestInvoiceId: invoiceId, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: `ch_${invoiceId}`, - created: now - 300, - invoiceId, - paymentIntentId: `pi_${invoiceId}`, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorPaymentIntent({ - chargeId: `ch_${invoiceId}`, - invoiceId, - paymentIntentId: `pi_${invoiceId}`, - stripeCustomerId, - }); - await mirrorCharge({ - chargeId: `ch_${invoiceId}`, - invoiceId, - paymentIntentId: `pi_${invoiceId}`, - stripeCustomerId, - }); - await mirrorPaymentMethod({paymentMethodId: 'pm_billing_metadata_only', stripeCustomerId}); - const overview = await createBuilder(harness, `${admin.token}`) - .get(`/admin/billing/users/${targetUser.userId}/overview`) - .execute(); - expect(overview.stripe_customer_id).toBe(stripeCustomerId); - expect(overview.subscription?.id).toBe(stripeSubscriptionId); - expect(overview.subscription?.status).toBe('active'); - expect(overview.payment_methods[0]?.id).toBe('pm_billing_metadata_only'); - expect(overview.payments[0]?.invoice_id).toBe(invoiceId); - expect(overview.payments[0]?.resolved_payment_intent_id).toBe(`pi_${invoiceId}`); - }); - test('cancels a Stripe subscription at period end after resolving missing local Stripe IDs from Stripe metadata', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), [ - 'admin:authenticate', - 'billing:manage_subscription', - ]); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_billing_cancel_metadata'; - const stripeSubscriptionId = 'sub_billing_cancel_metadata'; - const now = Math.floor(Date.now() / 1000); - const stripeHandlers = createStripeApiHandlers({ - customers: { - [stripeCustomerId]: { - metadata: { - userId: targetUser.userId, - }, - }, - }, - subscriptions: { - [stripeSubscriptionId]: { - customer: stripeCustomerId, - status: 'active', - current_period_start: now - DAY_SECONDS, - current_period_end: now + 29 * DAY_SECONDS, - }, - }, - }); - server.use(...stripeHandlers.handlers); - await mirrorCustomer({stripeCustomerId, userId: targetUser.userId}); - await mirrorSubscription({ - currentPeriodEnd: now + 29 * DAY_SECONDS, - currentPeriodStart: now - DAY_SECONDS, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${targetUser.userId}/cancel-subscription`) - .body({}) - .expect(204) - .execute(); - expect(stripeHandlers.spies.updatedSubscriptions).toContainEqual({ - id: stripeSubscriptionId, - params: { - cancel_at_period_end: 'true', - }, - }); - }); - test('allows admin refunds when the payment intent belongs to the target Stripe customer even without a local payment-intent index', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), [ - 'admin:authenticate', - 'billing:refund', - ]); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_refund_target'; - await setStripeCustomerId(targetUser.userId, stripeCustomerId); - const stripeHandlers = createStripeApiHandlers({ - paymentIntents: { - pi_remote_only_refund: { - customer: stripeCustomerId, - latest_charge: 'ch_remote_only_refund', - }, - }, - }); - server.use(...stripeHandlers.handlers); - await mirrorPaymentIntent({ - chargeId: 'ch_remote_only_refund', - paymentIntentId: 'pi_remote_only_refund', - stripeCustomerId, - }); - await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${targetUser.userId}/refund`) - .body({ - payment_intent_id: 'pi_remote_only_refund', - reason: 'Customer requested a refund', - }) - .expect(204) - .execute(); - expect(stripeHandlers.spies.createdRefunds).toHaveLength(1); - expect(stripeHandlers.spies.createdRefunds[0]).toMatchObject({ - payment_intent: 'pi_remote_only_refund', - reason: 'requested_by_customer', - metadata: { - admin_user_id: admin.userId, - target_user_id: targetUser.userId, - admin_reason: 'Customer requested a refund', - }, - }); - }); - test('forces a full refund when the latest invoice is inside the EU withdrawal window without a waiver', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), [ - 'admin:authenticate', - 'billing:refund', - 'billing:manage_subscription', - ]); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_eu_missing_waiver'; - const stripeSubscriptionId = 'sub_eu_missing_waiver'; - const invoiceId = 'in_eu_missing_waiver'; - await setStripeSubscriptionState({userId: targetUser.userId, stripeCustomerId, stripeSubscriptionId}); - await createPaymentRecord({ - userId: targetUser.userId, - checkoutSessionId: 'cs_eu_missing_waiver', - completedAt: new Date(Date.now() - 6 * DAY_SECONDS * 1000), - euWithdrawalWaiverRequired: true, - euWithdrawalWaiverAccepted: false, - euWithdrawalWaiverTextVersion: '2026-04-23', - invoiceId, - purchaseClientCountryCode: 'DE', - purchaseGeoipCountryCode: 'DE', - subscriptionId: stripeSubscriptionId, - stripeCustomerId, - }); - const stripeHandlers = createRefundPolicyStripeHandlers({ - amountPaidCents: 499, - elapsedDays: 6, - invoiceId, - stripeCustomerId, - stripeSubscriptionId, - }); - server.use(...stripeHandlers.handlers); - const now = Math.floor(Date.now() / 1000); - await mirrorSubscription({ - currentPeriodEnd: now + 24 * DAY_SECONDS, - currentPeriodStart: now - 6 * DAY_SECONDS, - latestInvoiceId: invoiceId, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: 'ch_eu_missing_waiver', - invoiceId, - paymentIntentId: 'pi_eu_missing_waiver', - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorCharge({ - chargeId: 'ch_eu_missing_waiver', - invoiceId, - paymentIntentId: 'pi_eu_missing_waiver', - stripeCustomerId, - }); - const result = await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${targetUser.userId}/refund-policy-cancel-now`) - .body({reason: 'Withdrawal waiver missing'}) - .execute(); - expect(result.refund_policy).toBe('full_refund'); - expect(result.refund_policy_basis).toBe('eu_eea_withdrawal_no_waiver'); - expect(result.refunded_amount_cents).toBe(499); - expect(result.eu_withdrawal_waiver_required).toBe(true); - expect(result.eu_withdrawal_waiver_accepted).toBe(false); - expect(result.purchase_geoip_country_code).toBe('DE'); - expect(stripeHandlers.spies.createdRefunds[0]).toMatchObject({ - amount: '499', - metadata: { - refund_policy: 'full_refund', - refund_policy_basis: 'eu_eea_withdrawal_no_waiver', - eu_withdrawal_waiver_required: 'true', - eu_withdrawal_waiver_accepted: 'false', - }, - }); - expect(stripeHandlers.spies.cancelledSubscriptions).toContain(stripeSubscriptionId); - }); - test('uses the support prorate policy when an EU waiver was accepted', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), [ - 'admin:authenticate', - 'billing:refund', - 'billing:manage_subscription', - ]); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_eu_accepted_waiver'; - const stripeSubscriptionId = 'sub_eu_accepted_waiver'; - const invoiceId = 'in_eu_accepted_waiver'; - await setStripeSubscriptionState({userId: targetUser.userId, stripeCustomerId, stripeSubscriptionId}); - await createPaymentRecord({ - userId: targetUser.userId, - checkoutSessionId: 'cs_eu_accepted_waiver', - completedAt: new Date(Date.now() - 6 * DAY_SECONDS * 1000), - euWithdrawalWaiverRequired: true, - euWithdrawalWaiverAccepted: true, - euWithdrawalWaiverAcceptedAt: new Date(Date.now() - 6 * DAY_SECONDS * 1000), - euWithdrawalWaiverTextVersion: '2026-04-23', - invoiceId, - purchaseClientCountryCode: 'DE', - purchaseGeoipCountryCode: 'DE', - subscriptionId: stripeSubscriptionId, - stripeCustomerId, - }); - const stripeHandlers = createRefundPolicyStripeHandlers({ - amountPaidCents: 499, - elapsedDays: 6, - invoiceId, - stripeCustomerId, - stripeSubscriptionId, - }); - server.use(...stripeHandlers.handlers); - const now = Math.floor(Date.now() / 1000); - await mirrorSubscription({ - currentPeriodEnd: now + 24 * DAY_SECONDS, - currentPeriodStart: now - 6 * DAY_SECONDS, - latestInvoiceId: invoiceId, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: 'ch_eu_accepted_waiver', - invoiceId, - paymentIntentId: 'pi_eu_accepted_waiver', - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorCharge({ - chargeId: 'ch_eu_accepted_waiver', - invoiceId, - paymentIntentId: 'pi_eu_accepted_waiver', - stripeCustomerId, - }); - const result = await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${targetUser.userId}/refund-policy-cancel-now`) - .body({}) - .execute(); - expect(result.refund_policy).toBe('prorated_refund'); - expect(result.refund_policy_basis).toBe('support_policy'); - expect(result.refunded_amount_cents).toBe(400); - expect(stripeHandlers.spies.createdRefunds[0]).toMatchObject({ - amount: '400', - metadata: { - refund_policy: 'prorated_refund', - refund_policy_basis: 'support_policy', - eu_withdrawal_waiver_required: 'true', - eu_withdrawal_waiver_accepted: 'true', - }, - }); - expect(stripeHandlers.spies.cancelledSubscriptions).toContain(stripeSubscriptionId); - }); - test('cancels without refund after the support refund window', async () => { - const admin = await setUserACLs(harness, await createTestAccount(harness), [ - 'admin:authenticate', - 'billing:refund', - 'billing:manage_subscription', - ]); - const targetUser = await createTestAccount(harness); - const stripeCustomerId = 'cus_cancel_only'; - const stripeSubscriptionId = 'sub_cancel_only'; - const invoiceId = 'in_cancel_only'; - await setStripeSubscriptionState({userId: targetUser.userId, stripeCustomerId, stripeSubscriptionId}); - await createPaymentRecord({ - userId: targetUser.userId, - checkoutSessionId: 'cs_cancel_only', - completedAt: new Date(Date.now() - 20 * DAY_SECONDS * 1000), - invoiceId, - subscriptionId: stripeSubscriptionId, - stripeCustomerId, - }); - const stripeHandlers = createRefundPolicyStripeHandlers({ - amountPaidCents: 499, - elapsedDays: 20, - invoiceId, - stripeCustomerId, - stripeSubscriptionId, - }); - server.use(...stripeHandlers.handlers); - const now = Math.floor(Date.now() / 1000); - await mirrorSubscription({ - currentPeriodEnd: now + 10 * DAY_SECONDS, - currentPeriodStart: now - 20 * DAY_SECONDS, - latestInvoiceId: invoiceId, - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorInvoice({ - amountPaidCents: 499, - chargeId: 'ch_cancel_only', - invoiceId, - paymentIntentId: 'pi_cancel_only', - stripeCustomerId, - stripeSubscriptionId, - userId: targetUser.userId, - }); - await mirrorCharge({ - chargeId: 'ch_cancel_only', - invoiceId, - paymentIntentId: 'pi_cancel_only', - stripeCustomerId, - }); - const result = await createBuilder(harness, `${admin.token}`) - .post(`/admin/billing/users/${targetUser.userId}/refund-policy-cancel-now`) - .body({}) - .execute(); - expect(result.refund_policy).toBe('cancel_only'); - expect(result.refund_policy_basis).toBe('support_policy'); - expect(result.refunded_amount_cents).toBe(0); - expect(stripeHandlers.spies.createdRefunds).toHaveLength(0); - expect(stripeHandlers.spies.cancelledSubscriptions).toContain(stripeSubscriptionId); - }); -}); diff --git a/packages/constants/src/AdminACLs.ts b/packages/constants/src/AdminACLs.ts index d15bd120c..8c2162949 100644 --- a/packages/constants/src/AdminACLs.ts +++ b/packages/constants/src/AdminACLs.ts @@ -42,9 +42,6 @@ export const AdminACLs = { BAN_PROFILE_SUBSTRING_ADD: 'ban:profile_substring:add', BAN_PROFILE_SUBSTRING_CHECK: 'ban:profile_substring:check', BAN_PROFILE_SUBSTRING_REMOVE: 'ban:profile_substring:remove', - BILLING_MANAGE_SUBSCRIPTION: 'billing:manage_subscription', - BILLING_REFUND: 'billing:refund', - BILLING_VIEW: 'billing:view', BULK_ADD_GUILD_MEMBERS: 'bulk:add:guild_members', BULK_DELETE_USERS: 'bulk:delete:users', BULK_UPDATE_GUILD_FEATURES: 'bulk:update:guild_features', diff --git a/packages/schema/src/domains/admin/AdminBillingSchemas.ts b/packages/schema/src/domains/admin/AdminBillingSchemas.ts deleted file mode 100644 index c43624af1..000000000 --- a/packages/schema/src/domains/admin/AdminBillingSchemas.ts +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {z} from 'zod'; - -const AdminPaymentRefundResponse = z.object({ - id: z.string(), - amount_cents: z.number(), - currency: z.string(), - status: z.string().nullable(), - reason: z.string().nullable(), - created: z.number(), - payment_intent_id: z.string().nullable(), - charge_id: z.string().nullable(), -}); - -const AdminPaymentResponse = z.object({ - checkout_session_id: z.string().nullable(), - user_id: z.string(), - stripe_customer_id: z.string().nullable(), - payment_intent_id: z.string().nullable(), - resolved_payment_intent_id: z.string().nullable(), - charge_id: z.string().nullable(), - subscription_id: z.string().nullable(), - invoice_id: z.string().nullable(), - price_id: z.string().nullable(), - product_type: z.string().nullable(), - amount_cents: z.number(), - currency: z.string(), - status: z.string(), - stripe_source: z.enum(['invoice']), - refundable_via_payment_intent: z.boolean(), - refunded_amount_cents: z.number(), - net_amount_cents: z.number(), - refunds: z.array(AdminPaymentRefundResponse), - payment_method_type: z.string().nullable(), - payment_method_brand: z.string().nullable(), - payment_method_last4: z.string().nullable(), - stripe_payment_method_country_code: z.string().nullable(), - stripe_billing_country_code: z.string().nullable(), - stripe_customer_country_code: z.string().nullable(), - stripe_terms_of_service_accepted: z.boolean().nullable(), - is_gift: z.boolean(), - gift_code: z.string().nullable(), - purchase_geoip_country_code: z.string().nullable(), - purchase_client_country_code: z.string().nullable(), - eu_withdrawal_waiver_required: z.boolean(), - eu_withdrawal_waiver_accepted: z.boolean(), - eu_withdrawal_waiver_accepted_at: z.string().nullable(), - eu_withdrawal_waiver_text_version: z.string().nullable(), - created_at: z.string(), - completed_at: z.string().nullable(), -}); - -export const AdminPaymentListResponse = z.object({ - payments: z.array(AdminPaymentResponse), -}); - -export type AdminPaymentListResponse = z.infer; - -export const AdminSubscriptionResponse = z.object({ - id: z.string(), - status: z.string(), - current_period_start: z.string().nullable(), - current_period_end: z.string().nullable(), - cancel_at_period_end: z.boolean(), - cancel_at: z.string().nullable(), - canceled_at: z.string().nullable(), - plan_interval: z.string().nullable(), - plan_amount_cents: z.number().nullable(), - plan_currency: z.string().nullable(), - default_payment_method_id: z.string().nullable(), -}); - -export type AdminSubscriptionResponse = z.infer; - -const AdminPaymentMethodResponse = z.object({ - id: z.string(), - type: z.string(), - card_brand: z.string().nullable(), - card_last4: z.string().nullable(), - card_exp_month: z.number().nullable(), - card_exp_year: z.number().nullable(), - created: z.number(), -}); - -export const AdminPaymentMethodListResponse = z.object({ - payment_methods: z.array(AdminPaymentMethodResponse), -}); - -export type AdminPaymentMethodListResponse = z.infer; - -const AdminInvoiceResponse = z.object({ - id: z.string(), - amount_due: z.number(), - amount_paid: z.number(), - currency: z.string(), - status: z.string().nullable(), - created: z.number(), - billing_reason: z.string().nullable(), - subscription_id: z.string().nullable(), - payment_type: z.string().nullable(), - payment_status: z.string().nullable(), - payment_intent_id: z.string().nullable(), - charge_id: z.string().nullable(), - paid_at: z.string().nullable(), - hosted_invoice_url: z.string().nullable(), - invoice_pdf: z.string().nullable(), -}); - -export const AdminInvoiceListResponse = z.object({ - invoices: z.array(AdminInvoiceResponse), - has_more: z.boolean(), -}); - -export type AdminInvoiceListResponse = z.infer; - -export const AdminBillingRefundRequest = z.object({ - payment_intent_id: z.string(), - amount_cents: z.number().int().positive().optional(), - reason: z.string().trim().min(1).max(512).optional(), -}); - -export type AdminBillingRefundRequest = z.infer; - -export const AdminBillingRefundLatestInvoiceCancelRequest = z.object({ - reason: z.string().trim().min(1).max(512).optional(), -}); - -export type AdminBillingRefundLatestInvoiceCancelRequest = z.infer; - -export const AdminBillingCancelImmediatelyRequest = z.object({ - reason: z.string().trim().min(1).max(512).optional(), -}); - -export type AdminBillingCancelImmediatelyRequest = z.infer; - -export const AdminBillingRefundLatestInvoiceCancelResponse = z.object({ - subscription_id: z.string(), - invoice_id: z.string(), - payment_intent_id: z.string().nullable(), - charge_id: z.string().nullable(), - refund_policy: z.enum(['full_refund', 'prorated_refund', 'cancel_only']), - refund_policy_basis: z.enum(['support_policy', 'eu_eea_withdrawal_no_waiver']), - refund_id: z.string().nullable(), - refunded_amount_cents: z.number(), - invoice_amount_paid_cents: z.number(), - currency: z.string(), - cycle_elapsed_days: z.number(), - purchase_geoip_country_code: z.string().nullable(), - purchase_client_country_code: z.string().nullable(), - stripe_payment_method_country_code: z.string().nullable(), - stripe_billing_country_code: z.string().nullable(), - stripe_customer_country_code: z.string().nullable(), - stripe_terms_of_service_accepted: z.boolean().nullable(), - eu_withdrawal_waiver_required: z.boolean(), - eu_withdrawal_waiver_accepted: z.boolean(), - eu_withdrawal_waiver_accepted_at: z.string().nullable(), - eu_withdrawal_waiver_text_version: z.string().nullable(), -}); - -export type AdminBillingRefundLatestInvoiceCancelResponse = z.infer< - typeof AdminBillingRefundLatestInvoiceCancelResponse ->; - -export const AdminBillingOverviewResponse = z.object({ - subscription: AdminSubscriptionResponse.nullable(), - payments: z.array(AdminPaymentResponse), - payment_methods: z.array(AdminPaymentMethodResponse), - stripe_customer_id: z.string().nullable(), -}); - -export type AdminBillingOverviewResponse = z.infer;