diff --git a/drizzle/0011_curvy_baron_strucker.sql b/drizzle/0011_curvy_baron_strucker.sql new file mode 100644 index 00000000..8d8959b2 --- /dev/null +++ b/drizzle/0011_curvy_baron_strucker.sql @@ -0,0 +1,2 @@ +ALTER TABLE "easyinvoice_client_payment" ADD COLUMN "conversionInfo" json;--> statement-breakpoint +ALTER TABLE "easyinvoice_request" ADD COLUMN "conversionInfo" json; \ No newline at end of file diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 00000000..2680fed1 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1228 @@ +{ + "id": "9c8cafdf-3fbf-4125-899d-6b19bc923272", + "prevId": "9c05fd19-afb4-41b6-820f-f4bbaef48195", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.easyinvoice_client_payment": { + "name": "easyinvoice_client_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestId": { + "name": "requestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ecommerceClientId": { + "name": "ecommerceClientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoiceCurrency": { + "name": "invoiceCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentCurrency": { + "name": "paymentCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "txHash": { + "name": "txHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customerInfo": { + "name": "customerInfo", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conversionInfo": { + "name": "conversionInfo", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "client_payment_request_id_tx_hash_unique": { + "name": "client_payment_request_id_tx_hash_unique", + "columns": [ + { + "expression": "requestId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "txHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "easyinvoice_client_payment_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_client_payment_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_client_payment", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_client_payment_ecommerceClientId_easyinvoice_ecommerce_client_id_fk": { + "name": "easyinvoice_client_payment_ecommerceClientId_easyinvoice_ecommerce_client_id_fk", + "tableFrom": "easyinvoice_client_payment", + "tableTo": "easyinvoice_ecommerce_client", + "columnsFrom": ["ecommerceClientId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_ecommerce_client": { + "name": "easyinvoice_ecommerce_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rnClientId": { + "name": "rnClientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feeAddress": { + "name": "feeAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feePercentage": { + "name": "feePercentage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "ecommerce_client_user_id_domain_unique": { + "name": "ecommerce_client_user_id_domain_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ecommerce_client_user_id_client_id_unique": { + "name": "ecommerce_client_user_id_client_id_unique", + "columns": [ + { + "expression": "rnClientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "easyinvoice_ecommerce_client_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_ecommerce_client_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_ecommerce_client", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_invoice_me": { + "name": "easyinvoice_invoice_me", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_invoice_me_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_invoice_me_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_invoice_me", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_payment_details_payers": { + "name": "easyinvoice_payment_details_payers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paymentDetailsId": { + "name": "paymentDetailsId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payerId": { + "name": "payerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_details_status": { + "name": "payment_details_status", + "type": "payment_details_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "externalPaymentDetailId": { + "name": "externalPaymentDetailId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_payment_details_payers_paymentDetailsId_easyinvoice_payment_details_id_fk": { + "name": "easyinvoice_payment_details_payers_paymentDetailsId_easyinvoice_payment_details_id_fk", + "tableFrom": "easyinvoice_payment_details_payers", + "tableTo": "easyinvoice_payment_details", + "columnsFrom": ["paymentDetailsId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_payment_details_payers_payerId_easyinvoice_user_id_fk": { + "name": "easyinvoice_payment_details_payers_payerId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_payment_details_payers", + "tableTo": "easyinvoice_user", + "columnsFrom": ["payerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_payment_details": { + "name": "easyinvoice_payment_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bankName": { + "name": "bankName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accountName": { + "name": "accountName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accountNumber": { + "name": "accountNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routingNumber": { + "name": "routingNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_type": { + "name": "account_type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'checking'" + }, + "sortCode": { + "name": "sortCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "iban": { + "name": "iban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swiftBic": { + "name": "swiftBic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentNumber": { + "name": "documentNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentType": { + "name": "documentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ribNumber": { + "name": "ribNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bsbNumber": { + "name": "bsbNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ncc": { + "name": "ncc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branchCode": { + "name": "branchCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankCode": { + "name": "bankCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ifsc": { + "name": "ifsc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "beneficiary_type": { + "name": "beneficiary_type", + "type": "beneficiary_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dateOfBirth": { + "name": "dateOfBirth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "addressLine2": { + "name": "addressLine2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rails_type": { + "name": "rails_type", + "type": "rails_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "businessActivity": { + "name": "businessActivity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nationality": { + "name": "nationality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_payment_details_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_payment_details_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_payment_details", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_recurring_payment": { + "name": "easyinvoice_recurring_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "externalPaymentId": { + "name": "externalPaymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "recurring_payment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "totalAmount": { + "name": "totalAmount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentCurrency": { + "name": "paymentCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain": { + "name": "chain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totalNumberOfPayments": { + "name": "totalNumberOfPayments", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currentNumberOfPayments": { + "name": "currentNumberOfPayments", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscriptionId": { + "name": "subscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payer": { + "name": "payer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recurrence": { + "name": "recurrence", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "payments": { + "name": "payments", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_recurring_payment_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_recurring_payment_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_recurring_payment", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_recurring_payment_subscriptionId_easyinvoice_subscription_plans_id_fk": { + "name": "easyinvoice_recurring_payment_subscriptionId_easyinvoice_subscription_plans_id_fk", + "tableFrom": "easyinvoice_recurring_payment", + "tableTo": "easyinvoice_subscription_plans", + "columnsFrom": ["subscriptionId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_request": { + "name": "easyinvoice_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuedDate": { + "name": "issuedDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientName": { + "name": "clientName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientEmail": { + "name": "clientEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creatorName": { + "name": "creatorName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creatorEmail": { + "name": "creatorEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "items": { + "name": "items", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoiceCurrency": { + "name": "invoiceCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentCurrency": { + "name": "paymentCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payee": { + "name": "payee", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestId": { + "name": "requestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentReference": { + "name": "paymentReference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "originalRequestPaymentReference": { + "name": "originalRequestPaymentReference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "originalRequestId": { + "name": "originalRequestId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoicedTo": { + "name": "invoicedTo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recurrence": { + "name": "recurrence", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "isRecurrenceStopped": { + "name": "isRecurrenceStopped", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "isCryptoToFiatAvailable": { + "name": "isCryptoToFiatAvailable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "paymentDetailsId": { + "name": "paymentDetailsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conversionInfo": { + "name": "conversionInfo", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_request_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_request_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_request", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_request_paymentDetailsId_easyinvoice_payment_details_id_fk": { + "name": "easyinvoice_request_paymentDetailsId_easyinvoice_payment_details_id_fk", + "tableFrom": "easyinvoice_request", + "tableTo": "easyinvoice_payment_details", + "columnsFrom": ["paymentDetailsId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_session": { + "name": "easyinvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_session_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_session_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_session", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_subscription_plans": { + "name": "easyinvoice_subscription_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trialDays": { + "name": "trialDays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "paymentCurrency": { + "name": "paymentCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain": { + "name": "chain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totalNumberOfPayments": { + "name": "totalNumberOfPayments", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "frequency_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_subscription_plans_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_subscription_plans_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_subscription_plans", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_user": { + "name": "easyinvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "googleId": { + "name": "googleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agreement_status": { + "name": "agreement_status", + "type": "agreement_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "kyc_status": { + "name": "kyc_status", + "type": "kyc_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "isCompliant": { + "name": "isCompliant", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "easyinvoice_user_googleId_unique": { + "name": "easyinvoice_user_googleId_unique", + "nullsNotDistinct": false, + "columns": ["googleId"] + }, + "easyinvoice_user_email_unique": { + "name": "easyinvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": ["checking", "savings"] + }, + "public.agreement_status": { + "name": "agreement_status", + "schema": "public", + "values": ["not_started", "pending", "completed"] + }, + "public.beneficiary_type": { + "name": "beneficiary_type", + "schema": "public", + "values": ["individual", "business"] + }, + "public.frequency_enum": { + "name": "frequency_enum", + "schema": "public", + "values": ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": ["male", "female", "other", "prefer_not_to_say"] + }, + "public.kyc_status": { + "name": "kyc_status", + "schema": "public", + "values": ["not_started", "initiated", "pending", "approved"] + }, + "public.payment_details_status": { + "name": "payment_details_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.rails_type": { + "name": "rails_type", + "schema": "public", + "values": ["local", "swift", "wire"] + }, + "public.recurring_payment_status": { + "name": "recurring_payment_status", + "schema": "public", + "values": ["pending", "active", "paused", "completed", "cancelled"] + }, + "public.request_status": { + "name": "request_status", + "schema": "public", + "values": [ + "pending", + "paid", + "crypto_paid", + "offramp_initiated", + "offramp_failed", + "offramp_pending", + "processing", + "overdue" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 06fe14ef..57d4c0d6 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1759497538341, "tag": "0010_peaceful_lionheart", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1760624623200, + "tag": "0011_curvy_baron_strucker", + "breakpoints": true } ] } diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index 217713ea..9dee4756 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -2,13 +2,16 @@ import crypto from "node:crypto"; import { ResourceNotFoundError } from "@/lib/errors"; import { generateInvoiceNumber } from "@/lib/helpers/client"; import { getInvoiceCount } from "@/lib/helpers/invoice"; +import type { ConversionInfo } from "@/lib/types"; import { db } from "@/server/db"; import { + type ClientPayment, + type RecurringPaymentInstallment, + type Request as RequestModel, clientPaymentTable, ecommerceClientTable, paymentDetailsPayersTable, recurringPaymentTable, - type requestStatusEnum, requestTable, userTable, } from "@/server/db/schema"; @@ -16,32 +19,60 @@ import { and, eq, not } from "drizzle-orm"; import { NextResponse } from "next/server"; import { ulid } from "ulid"; -async function addClientPayment(webhookBody: any) { +type ClientPaymentBody = Omit< + ClientPayment, + "id" | "userId" | "ecommerceClientId" | "createdAt" +>; + +function getClientPaymentBody( + webhookBody: any, + conversionInfo: ConversionInfo | null, +): ClientPaymentBody { + const requiredFields = [ + "requestId", + "currency", + "paymentCurrency", + "txHash", + "network", + "amount", + ]; + + const missingFields = requiredFields.filter((field) => !webhookBody[field]); + if (missingFields.length > 0) { + throw new Error( + `Missing required webhook fields: ${missingFields.join(", ")}`, + ); + } + const clientPaymentBody: ClientPaymentBody = { + requestId: webhookBody.requestId, + invoiceCurrency: webhookBody.currency, + paymentCurrency: webhookBody.paymentCurrency, + txHash: webhookBody.txHash, + network: webhookBody.network, + amount: webhookBody.amount, + customerInfo: webhookBody.customerInfo || null, + reference: webhookBody.reference || null, + origin: webhookBody.origin, + conversionInfo, + }; + + return clientPaymentBody; +} + +async function addClientPayment( + clientPaymentBody: ClientPaymentBody, + clientId: string, +) { await db.transaction(async (tx) => { const ecommerceClient = await tx .select() .from(ecommerceClientTable) - .where(eq(ecommerceClientTable.rnClientId, webhookBody.clientId)) + .where(eq(ecommerceClientTable.rnClientId, clientId)) .limit(1); if (!ecommerceClient.length) { throw new ResourceNotFoundError( - `No ecommerce client found with client ID: ${webhookBody.clientId}`, - ); - } - const requiredFields = [ - "requestId", - "currency", - "paymentCurrency", - "txHash", - "network", - "amount", - ]; - - const missingFields = requiredFields.filter((field) => !webhookBody[field]); - if (missingFields.length > 0) { - throw new Error( - `Missing required webhook fields: ${missingFields.join(", ")}`, + `No ecommerce client found with client ID: ${clientId}`, ); } @@ -53,15 +84,7 @@ async function addClientPayment(webhookBody: any) { id: ulid(), userId: client.userId, ecommerceClientId: client.id, - requestId: webhookBody.requestId, - invoiceCurrency: webhookBody.currency, - paymentCurrency: webhookBody.paymentCurrency, - txHash: webhookBody.txHash, - network: webhookBody.network, - amount: webhookBody.amount, - customerInfo: webhookBody.customerInfo || null, - reference: webhookBody.reference || null, - origin: webhookBody.origin, + ...clientPaymentBody, }) .onConflictDoNothing({ target: [clientPaymentTable.requestId, clientPaymentTable.txHash], @@ -70,7 +93,7 @@ async function addClientPayment(webhookBody: any) { if (!inserted.length) { console.warn( - `Duplicate client payment detected for requestId: ${webhookBody.requestId} and txHash: ${webhookBody.txHash}`, + `Duplicate client payment detected for requestId: ${clientPaymentBody.requestId} and txHash: ${clientPaymentBody.txHash}`, ); return; } @@ -80,14 +103,14 @@ async function addClientPayment(webhookBody: any) { /** * Updates the request status in the database */ -async function updateRequestStatus( +async function updateRequest( requestId: string, - status: (typeof requestStatusEnum.enumValues)[number], + requestData: Partial, ) { await db.transaction(async (tx) => { const result = await tx .update(requestTable) - .set({ status }) + .set({ ...requestData }) .where(eq(requestTable.requestId, requestId)) .returning({ id: requestTable.id }); @@ -104,11 +127,7 @@ async function updateRequestStatus( */ async function addPaymentToRecurringPayment( externalPaymentId: string, - payment: { - date: string; - txHash: string; - requestScanUrl?: string; - }, + payment: RecurringPaymentInstallment, ) { await db.transaction(async (tx) => { const recurringPayments = await tx @@ -145,6 +164,28 @@ async function addPaymentToRecurringPayment( }); } +function getConversionInfo(webhookBody: any): ConversionInfo | null { + if ( + !webhookBody.conversionRate || + !webhookBody.convertedAmountSource || + !webhookBody.convertedAmountDestination || + !webhookBody.conversionRateSource || + !webhookBody.conversionRateDestination || + !webhookBody.rateProvider + ) { + return null; + } + + return { + conversionRate: webhookBody.conversionRate, + convertedAmountSource: webhookBody.convertedAmountSource, + convertedAmountDestination: webhookBody.convertedAmountDestination, + conversionRateSource: webhookBody.conversionRateSource, + conversionRateDestination: webhookBody.conversionRateDestination, + rateProvider: webhookBody.rateProvider, + }; +} + export async function POST(req: Request) { let webhookData: Record = {}; @@ -177,6 +218,8 @@ export async function POST(req: Request) { subStatus, } = body; + const conversionInfo = getConversionInfo(body); + switch (event) { case "payment.confirmed": // if this is defined, it's a payment that's part of a recurring payment @@ -194,32 +237,33 @@ export async function POST(req: Request) { date: body.timestamp, txHash: body.txHash, requestScanUrl: body.explorer, + conversionInfo, }); } else if (body.clientId) { - await addClientPayment(body); + const clientPaymentBody = getClientPaymentBody(body, conversionInfo); + await addClientPayment(clientPaymentBody, body.clientId); } else { - await updateRequestStatus( - requestId, - isCryptoToFiat ? "crypto_paid" : "paid", - ); + await updateRequest(requestId, { + status: isCryptoToFiat ? "crypto_paid" : "paid", + }); } break; case "payment.processing": switch (subStatus) { case "initiated": - await updateRequestStatus(requestId, "offramp_initiated"); + await updateRequest(requestId, { status: "offramp_initiated" }); break; case "failed": case "bounced": - await updateRequestStatus(requestId, "offramp_failed"); + await updateRequest(requestId, { status: "offramp_failed" }); break; case "pending_internal_assessment": case "ongoing_checks": case "sending_fiat": - await updateRequestStatus(requestId, "offramp_pending"); + await updateRequest(requestId, { status: "offramp_pending" }); break; case "fiat_sent": - await updateRequestStatus(requestId, "paid"); + await updateRequest(requestId, { status: "paid" }); break; default: { console.error( diff --git a/src/components/dashboard/invoices-received.tsx b/src/components/dashboard/invoices-received.tsx index 9328e9bf..28f11445 100644 --- a/src/components/dashboard/invoices-received.tsx +++ b/src/components/dashboard/invoices-received.tsx @@ -12,10 +12,7 @@ import { } from "@/components/ui/table/table"; import { NETWORK_TO_ID } from "@/lib/constants/chains"; import { handleBatchPayment } from "@/lib/helpers/batch-payment"; -import { - calculateTotalsByCurrency, - formatCurrencyTotals, -} from "@/lib/helpers/currency"; +import { consolidateRequestUsdValues } from "@/lib/helpers/conversion"; import { useSwitchNetwork } from "@/lib/hooks/use-switch-network"; import type { Request } from "@/server/db/schema"; import { api } from "@/trpc/react"; @@ -28,7 +25,6 @@ import { ethers } from "ethers"; import { AlertCircle, DollarSign, FileText } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; -import { MultiCurrencyStatCard } from "../multi-currency-stat-card"; import { StatCard } from "../stat-card"; import { EmptyState } from "../ui/table/empty-state"; import { Pagination } from "../ui/table/pagination"; @@ -82,14 +78,12 @@ export const InvoicesReceived = ({ refetchInterval: RETRIEVE_ALL_INVOICES_POLLING_INTERVAL, }); - const invoiceItems = - invoices?.map((invoice) => ({ - amount: invoice.amount, - currency: invoice.paymentCurrency, - })) || []; + const unpaidInvoices = (invoices || []).filter( + (inv) => inv.status !== "paid", + ); + const { totalInUsd, hasNonUsdValues } = + consolidateRequestUsdValues(unpaidInvoices); - const totalsByCurrency = calculateTotalsByCurrency(invoiceItems); - const totalValues = formatCurrencyTotals(totalsByCurrency); const outstanding = invoices?.filter((inv) => inv.status !== "paid").length || 0; @@ -219,11 +213,20 @@ export const InvoicesReceived = ({ value={outstanding} icon={} /> - } - values={totalValues} - /> +
+ } + /> + {hasNonUsdValues && ( +
+

+ * Excludes non-USD invoices without conversion info +

+
+ )} +
diff --git a/src/components/dashboard/invoices-sent.tsx b/src/components/dashboard/invoices-sent.tsx index 8b430e6c..c31d1fb5 100644 --- a/src/components/dashboard/invoices-sent.tsx +++ b/src/components/dashboard/invoices-sent.tsx @@ -9,16 +9,12 @@ import { TableHeader, TableRow, } from "@/components/ui/table/table"; -import { - calculateTotalsByCurrency, - formatCurrencyTotals, -} from "@/lib/helpers/currency"; +import { consolidateRequestUsdValues } from "@/lib/helpers/conversion"; import type { Request } from "@/server/db/schema"; import { api } from "@/trpc/react"; import { AlertCircle, DollarSign, FileText, Plus } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; -import { MultiCurrencyStatCard } from "../multi-currency-stat-card"; import { StatCard } from "../stat-card"; import { EmptyState } from "../ui/table/empty-state"; import { Pagination } from "../ui/table/pagination"; @@ -54,14 +50,9 @@ export const InvoicesSent = ({ initialSentInvoices }: InvoicesSentProps) => { refetchInterval: RETRIEVE_ALL_INVOICES_POLLING_INTERVAL, }); - const invoiceItems = - invoices?.map((invoice) => ({ - amount: invoice.amount, - currency: invoice.paymentCurrency, - })) || []; - - const totalsByCurrency = calculateTotalsByCurrency(invoiceItems); - const totalValues = formatCurrencyTotals(totalsByCurrency); + const { totalInUsd, hasNonUsdValues } = consolidateRequestUsdValues( + invoices || [], + ); const outstanding = invoices?.filter((inv) => inv.status !== "paid").length || 0; @@ -79,11 +70,20 @@ export const InvoicesSent = ({ initialSentInvoices }: InvoicesSentProps) => { value={outstanding} icon={} /> - } - values={totalValues} - /> +
+ } + /> + {hasNonUsdValues && ( +
+

+ * Excludes non-USD invoices without conversion info +

+
+ )} +
diff --git a/src/components/dashboard/subscriptions.tsx b/src/components/dashboard/subscriptions.tsx index 152f1da6..40906541 100644 --- a/src/components/dashboard/subscriptions.tsx +++ b/src/components/dashboard/subscriptions.tsx @@ -22,10 +22,7 @@ import { import { CompletedPayments } from "@/components/view-recurring-payments/blocks/completed-payments"; import { FrequencyBadge } from "@/components/view-recurring-payments/blocks/frequency-badge"; import { formatCurrencyLabel } from "@/lib/constants/currencies"; -import { - calculateTotalsByCurrency, - formatCurrencyTotals, -} from "@/lib/helpers/currency"; +import { consolidateRecurringPaymentUsdValues } from "@/lib/helpers/conversion"; import { useCancelRecurringPayment } from "@/lib/hooks/use-cancel-recurring-payment"; import type { SubscriptionWithDetails } from "@/lib/types"; import { getCanCancelPayment } from "@/lib/utils"; @@ -33,7 +30,6 @@ import { api } from "@/trpc/react"; import { addDays, format } from "date-fns"; import { Ban, CreditCard, DollarSign, Loader2 } from "lucide-react"; import { useState } from "react"; -import { MultiCurrencyStatCard } from "../multi-currency-stat-card"; import { StatCard } from "../stat-card"; import { Button } from "../ui/button"; import { EmptyState } from "../ui/table/empty-state"; @@ -62,7 +58,9 @@ const SubscriptionTableColumns = () => ( const SubscriptionRow = ({ subscription, -}: { subscription: SubscriptionWithDetails }) => { +}: { + subscription: SubscriptionWithDetails; +}) => { const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); const utils = api.useUtils(); @@ -79,7 +77,6 @@ const SubscriptionRow = ({ try { await cancelRecurringPayment(subscription); } catch (error) { - // Error is already handled by the hook, but we need to catch it here console.error("Failed to cancel subscription:", error); } }; @@ -209,49 +206,83 @@ export const Subscriptions = ({ initialSubscriptions }: SubscriptionProps) => { initialData: initialSubscriptions, }); - const commitmentItems = - subscriptions - ?.filter((sub) => ACTIVE_STATUSES.includes(sub.status)) - .map((sub) => ({ - amount: sub.totalAmount, - currency: sub.paymentCurrency, - })) || []; + const activeSubscriptions = + subscriptions?.filter((sub) => ACTIVE_STATUSES.includes(sub.status)) || []; + + const paidSubscriptions = + subscriptions?.filter((sub) => sub.payments && sub.payments.length > 0) || + []; + + let commitmentTotal = 0; + let hasNonUsdCommitments = false; - const spentItems = - subscriptions - ?.filter((sub) => (sub?.payments ? sub.payments.length > 0 : false)) - .flatMap((sub) => ({ - amount: sub.totalAmount, - currency: sub.paymentCurrency, - })) || []; + for (const sub of activeSubscriptions) { + if (!sub.payments || sub.payments.length === 0) continue; + const { totalInUsd, hasNonUsdValues } = + consolidateRecurringPaymentUsdValues( + sub.totalAmount, + sub.paymentCurrency, + sub.payments, + ); + commitmentTotal += Number(totalInUsd); + if (hasNonUsdValues) { + hasNonUsdCommitments = true; + } + } - const commitmentTotals = calculateTotalsByCurrency(commitmentItems); - const spentTotals = calculateTotalsByCurrency(spentItems); + let spentTotal = 0; + let hasNonUsdSpent = false; - const commitmentValues = formatCurrencyTotals(commitmentTotals); - const spentValues = formatCurrencyTotals(spentTotals); + for (const sub of paidSubscriptions) { + if (!sub.payments || sub.payments.length === 0) continue; + const { totalInUsd, hasNonUsdValues } = + consolidateRecurringPaymentUsdValues( + sub.totalAmount, + sub.paymentCurrency, + sub.payments, + ); + spentTotal += Number(totalInUsd); + if (hasNonUsdValues) { + hasNonUsdSpent = true; + } + } return (
ACTIVE_STATUSES.includes(sub.status)) - .length || 0 - } + value={activeSubscriptions.length} icon={} /> - } - values={commitmentValues} - /> - } - values={spentValues} - /> +
+ } + /> + {hasNonUsdCommitments && ( +
+

+ * Excludes non-USD denominated subscriptions +

+
+ )} +
+
+ } + /> + {hasNonUsdSpent && ( +
+

+ * Excludes non-USD denominated payments +

+
+ )} +
diff --git a/src/components/multi-currency-stat-card.tsx b/src/components/multi-currency-stat-card.tsx deleted file mode 100644 index ed86ecda..00000000 --- a/src/components/multi-currency-stat-card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Card } from "@/components/ui/card"; - -interface MultiCurrencyStatCardProps { - title: string; - icon: React.ReactNode; - values: Array<{ - amount: string; - currency: string; - }>; -} - -export function MultiCurrencyStatCard({ - title, - icon, - values, -}: MultiCurrencyStatCardProps) { - return ( - -
- {icon} -

{title}

-
-
- {values.length === 0 ? ( -

--

- ) : ( - values.map(({ amount, currency }) => ( -
- - {amount} - - - {currency} - -
- )) - )} -
-
- ); -} diff --git a/src/components/subscription-plans/blocks/payments-table.tsx b/src/components/subscription-plans/blocks/payments-table.tsx index c4f31e19..dc95cbec 100644 --- a/src/components/subscription-plans/blocks/payments-table.tsx +++ b/src/components/subscription-plans/blocks/payments-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { MultiCurrencyStatCard } from "@/components/multi-currency-stat-card"; import { ShortAddress } from "@/components/short-address"; +import { StatCard } from "@/components/stat-card"; import { Card, CardContent } from "@/components/ui/card"; import { Select, @@ -19,10 +19,7 @@ import { TableRow, } from "@/components/ui/table/table"; import { formatCurrencyLabel } from "@/lib/constants/currencies"; -import { - calculateTotalsByCurrency, - formatCurrencyTotals, -} from "@/lib/helpers/currency"; +import { consolidateSubscriptionPaymentUsdValues } from "@/lib/helpers/conversion"; import type { SubscriptionPayment } from "@/lib/types"; import type { SubscriptionPlan } from "@/server/db/schema"; import { api } from "@/trpc/react"; @@ -36,7 +33,6 @@ import { Receipt, } from "lucide-react"; import { useState } from "react"; -import { StatCard } from "../../stat-card"; import { EmptyState } from "../../ui/table/empty-state"; import { Pagination } from "../../ui/table/pagination"; import { TableHeadCell } from "../../ui/table/table-head-cell"; @@ -133,10 +129,10 @@ export function PaymentsTable({ value="--" icon={} /> - } - values={[]} />
payment.planId === activePlan) : payments; - const paymentItems = filteredPayments.map((payment) => ({ - amount: payment.amount, - currency: payment.currency, - })); - - const revenueTotal = calculateTotalsByCurrency(paymentItems); - const revenueValues = formatCurrencyTotals(revenueTotal); + const { totalInUsd, hasNonUsdValues } = + consolidateSubscriptionPaymentUsdValues(filteredPayments); const paginatedPayments = filteredPayments.slice( (page - 1) * ITEMS_PER_PAGE, @@ -178,11 +169,20 @@ export function PaymentsTable({ value={filteredPayments.length} icon={} /> - } - values={revenueValues} - /> +
+ } + /> + {hasNonUsdValues && ( +
+

+ * Excludes non-USD invoices without conversion info +

+
+ )} +
diff --git a/src/components/subscription-plans/blocks/subscribers-table.tsx b/src/components/subscription-plans/blocks/subscribers-table.tsx index 60600507..2b1042f0 100644 --- a/src/components/subscription-plans/blocks/subscribers-table.tsx +++ b/src/components/subscription-plans/blocks/subscribers-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { MultiCurrencyStatCard } from "@/components/multi-currency-stat-card"; import { ShortAddress } from "@/components/short-address"; +import { StatCard } from "@/components/stat-card"; import { Card, CardContent } from "@/components/ui/card"; import { Select, @@ -21,18 +21,14 @@ import { import { CompletedPayments } from "@/components/view-recurring-payments/blocks/completed-payments"; import { FrequencyBadge } from "@/components/view-recurring-payments/blocks/frequency-badge"; import { formatCurrencyLabel } from "@/lib/constants/currencies"; -import { - calculateTotalsByCurrency, - formatCurrencyTotals, -} from "@/lib/helpers/currency"; +import { consolidateRecurringPaymentUsdValues } from "@/lib/helpers/conversion"; import type { SubscriptionWithDetails } from "@/lib/types"; import type { SubscriptionPlan } from "@/server/db/schema"; import { api } from "@/trpc/react"; import { addDays, format } from "date-fns"; import { utils } from "ethers"; -import { CreditCard, DollarSign, Filter } from "lucide-react"; +import { CreditCard, DollarSign, Filter, Users } from "lucide-react"; import { useState } from "react"; -import { StatCard } from "../../stat-card"; import { EmptyState } from "../../ui/table/empty-state"; import { TableHeadCell } from "../../ui/table/table-head-cell"; @@ -60,7 +56,9 @@ const SubscriberTableColumns = () => ( const SubscriberRow = ({ subscription, -}: { subscription: SubscriptionWithDetails }) => { +}: { + subscription: SubscriptionWithDetails; +}) => { const getTrialEndDate = () => { if (!subscription.subscription?.trialDays) return "No trial"; if (!subscription.createdAt) return "No trial"; @@ -157,10 +155,10 @@ export function SubscribersTable({ value="--" icon={} /> - } - values={[]} />
ACTIVE_STATUSES.includes(sub.status), - ).length; + ); - const revenueItems = filteredSubscribers - .filter( - (sub) => ACTIVE_STATUSES.includes(sub.status) && sub.payments?.length, - ) - .flatMap((sub) => ({ - amount: sub.totalAmount, - currency: sub.paymentCurrency, - })); + let totalRevenue = 0; + let hasNonUsdValues = false; - const revenueTotal = calculateTotalsByCurrency(revenueItems); - const revenueValues = formatCurrencyTotals(revenueTotal); + for (const sub of activeSubscribers) { + if (!sub.payments || sub.payments.length === 0) continue; + + const { totalInUsd, hasNonUsdValues: subHasNonUsdValues } = + consolidateRecurringPaymentUsdValues( + sub.totalAmount, + sub.paymentCurrency, + sub.payments || [], + ); + + totalRevenue += Number(totalInUsd); + if (subHasNonUsdValues) { + hasNonUsdValues = true; + } + } return (
} /> - } - values={revenueValues} - /> +
+ } + /> + {hasNonUsdValues && ( +
+

+ * Excludes non-USD subscriptions without conversion info +

+
+ )} +
@@ -247,9 +261,7 @@ export function SubscribersTable({ - } + icon={} title="No subscribers" subtitle={ activePlan diff --git a/src/components/view-recurring-payments/blocks/completed-payments.tsx b/src/components/view-recurring-payments/blocks/completed-payments.tsx index 927174b1..83131da5 100644 --- a/src/components/view-recurring-payments/blocks/completed-payments.tsx +++ b/src/components/view-recurring-payments/blocks/completed-payments.tsx @@ -3,12 +3,12 @@ import { ShortAddress } from "@/components/short-address"; import { Button } from "@/components/ui/button"; import { formatDate } from "@/lib/date-utils"; -import type { RecurringPayment } from "@/server/db/schema"; +import type { RecurringPaymentInstallment } from "@/server/db/schema"; import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react"; import { useState } from "react"; interface CompletedPaymentsProps { - payments: RecurringPayment["payments"]; + payments: RecurringPaymentInstallment[]; } export function CompletedPayments({ payments }: CompletedPaymentsProps) { diff --git a/src/lib/helpers/conversion.ts b/src/lib/helpers/conversion.ts new file mode 100644 index 00000000..006a80c4 --- /dev/null +++ b/src/lib/helpers/conversion.ts @@ -0,0 +1,109 @@ +import type { RecurringPaymentInstallment, Request } from "@/server/db/schema"; +import type { ConversionInfo, SubscriptionPayment } from "../types"; + +export type PaymentItem = { + amount: string; + currency: string; + conversionInfo: ConversionInfo | null; +}; + +export function convertRequestToPaymentItem(request: Request): PaymentItem { + return { + amount: request.amount, + currency: request.invoiceCurrency, + conversionInfo: request.conversionInfo, + }; +} + +export function convertRecurringPaymentInstallmentToPaymentItem( + recurringPaymentAmount: string, + recurringPaymentCurrency: string, + installment: RecurringPaymentInstallment, +): PaymentItem { + return { + amount: recurringPaymentAmount, + currency: recurringPaymentCurrency, + conversionInfo: installment.conversionInfo, + }; +} + +export function convertSubscriptionPaymentToPaymentItem( + subscriptionPayment: SubscriptionPayment, +): PaymentItem { + return { + amount: subscriptionPayment.amount, + currency: subscriptionPayment.currency, + conversionInfo: subscriptionPayment.conversionInfo, + }; +} + +const STABLECOINS = ["USDC", "USDT", "DAI", "USDCE", "fUSDC", "fUSDT", "FAU"]; // We add sepolia currencies for testing + +export function isStablecoin(currency: string): boolean { + const symbol = currency.split("-")[0]; + return STABLECOINS.includes(symbol); +} + +interface ConsolidatePaymentValuesResult { + totalInUsd: string; + hasNonUsdValues: boolean; +} +function consolidatePaymentUsdValues( + payments: PaymentItem[], +): ConsolidatePaymentValuesResult { + return { + totalInUsd: payments + .reduce((acc, item) => { + if (item.currency === "USD" || isStablecoin(item.currency)) { + return acc + Number(item.amount); + } + + if (item.conversionInfo) { + return acc + Number(item.conversionInfo.convertedAmountDestination); + } + + return acc; + }, 0) + .toFixed(2), + hasNonUsdValues: payments.some( + (item) => + item.currency !== "USD" && + !isStablecoin(item.currency) && + !item.conversionInfo, + ), + }; +} + +export function consolidateRequestUsdValues( + requests: Request[], +): ConsolidatePaymentValuesResult { + const paymentItems = requests.map(convertRequestToPaymentItem); + + return consolidatePaymentUsdValues(paymentItems); +} + +export function consolidateSubscriptionPaymentUsdValues( + subscriptionPayments: SubscriptionPayment[], +): ConsolidatePaymentValuesResult { + const paymentItems = subscriptionPayments.map( + convertSubscriptionPaymentToPaymentItem, + ); + + return consolidatePaymentUsdValues(paymentItems); +} + +export function consolidateRecurringPaymentUsdValues( + recurringPaymentAmount: string, + recurringPaymentCurrency: string, + installments: RecurringPaymentInstallment[], +): ConsolidatePaymentValuesResult { + const paymentItems = installments.map((installment) => + convertRecurringPaymentInstallmentToPaymentItem( + recurringPaymentAmount, + recurringPaymentCurrency, + installment, + ), + ); + + return consolidatePaymentUsdValues(paymentItems); +} diff --git a/src/lib/helpers/currency.ts b/src/lib/helpers/currency.ts deleted file mode 100644 index d70453e9..00000000 --- a/src/lib/helpers/currency.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BigNumber, utils } from "ethers"; - -interface PaymentItem { - amount: string; - currency: string; -} - -export function calculateTotalsByCurrency( - items: PaymentItem[], -): Record { - const totals = items.reduce( - (acc, item) => { - const currency = item.currency; - try { - const amount = utils.parseUnits(item.amount, 18); - if (!acc[currency]) { - acc[currency] = BigNumber.from("0"); - } - acc[currency] = acc[currency].add(amount); - } catch (error) { - console.error("Error calculating total:", error); - } - return acc; - }, - {} as Record, - ); - - return Object.entries(totals).reduce( - (acc, [currency, amount]) => { - acc[currency] = utils.formatUnits(amount, 18); - return acc; - }, - {} as Record, - ); -} - -export function formatCurrencyTotals( - totals: Record, -): Array<{ amount: string; currency: string }> { - return Object.entries(totals) - .map(([currency, amount]) => ({ amount, currency })) - .filter(({ amount }) => { - try { - return utils.parseUnits(amount, 18).gt(0); - } catch { - return false; - } - }); -} diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 97ab907c..c7f95a20 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -47,8 +47,18 @@ export type SubscriptionPayment = { subscriber: string; totalNumberOfPayments: number; paymentNumber: number; + conversionInfo: ConversionInfo | null; }; export type ClientPaymentWithEcommerceClient = inferRouterOutputs< typeof ecommerceRouter >["getAllClientPayments"][number]; + +export interface ConversionInfo { + conversionRate: string; + convertedAmountSource: string; + convertedAmountDestination: string; + conversionRateSource: string; + conversionRateDestination: string; + rateProvider: string; +} diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index f33d4caa..51005e6a 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,4 +1,5 @@ import { type EncryptionVersion, getEncryptionKey } from "@/lib/encryption"; +import type { ConversionInfo } from "@/lib/types"; import CryptoJS from "crypto-js"; import { type InferSelectModel, relations } from "drizzle-orm"; import { @@ -84,6 +85,13 @@ export const recurringPaymentStatusEnum = pgEnum( export type RecurringPaymentStatusType = (typeof RecurringPaymentStatus)[number]; +export type RecurringPaymentInstallment = { + date: string; + txHash: string; + requestScanUrl?: string; + conversionInfo: ConversionInfo | null; +}; + // biome-ignore lint/correctness/noUnusedVariables: This is a type definition that will be used in future database migrations const encryptedText = customType<{ data: string }>({ dataType() { @@ -214,6 +222,7 @@ export const requestTable = createTable("request", { paymentDetailsId: text().references(() => paymentDetailsTable.id, { onDelete: "set null", }), + conversionInfo: json().$type(), }); export const recurringPaymentTable = createTable("recurring_payment", { @@ -247,14 +256,7 @@ export const recurringPaymentTable = createTable("recurring_payment", { address: string; }>() .notNull(), - payments: - json().$type< - Array<{ - date: string; - txHash: string; - requestScanUrl?: string; - }> - >(), + payments: json().$type>(), }); export const sessionTable = createTable("session", { @@ -363,6 +365,7 @@ export const clientPaymentTable = createTable( reference: text(), origin: text(), createdAt: timestamp("created_at").defaultNow(), + conversionInfo: json().$type(), }, (table) => ({ requestIdTxHashIndex: uniqueIndex( diff --git a/src/server/routers/subscription-plan.ts b/src/server/routers/subscription-plan.ts index c67a49a8..de512214 100644 --- a/src/server/routers/subscription-plan.ts +++ b/src/server/routers/subscription-plan.ts @@ -199,6 +199,7 @@ export const subscriptionPlanRouter = router({ requestScanUrl: payment.requestScanUrl, chain: subscriber.chain, subscriber: subscriber.payer, + conversionInfo: payment.conversionInfo ?? null, }); }); }