HengHong
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Tazapay Provider Implementation Plan ## Overview This document provides a detailed implementation plan for integrating Tazapay as a uRamp provider, supporting both onramp and offramp flows for the following currencies: **Fiat Currencies**: USD, SGD, EUR, INR, BRL, PHP, THB, MXN, VND **Crypto Currencies**: USDC, USDT (on Ethereum, Polygon, Tron, Solana) --- ## === FILE STRUCTURE === ### 1. File-by-File Breakdown ``` packages/providers/tazapay/ ├── package.json # Package configuration ├── tsconfig.json # TypeScript configuration ├── manifest.ts # Plugin manifest with capabilities ├── index.ts # Main exports │ ├── client/ # API Client Layer │ ├── tazapay.api.service.ts # Tazapay API client │ ├── credential.schema.ts # Credential form schema │ ├── interfaces.ts # TypeScript interfaces │ └── config.ts # Configuration constants │ ├── api-server/ # Server-side hooks │ ├── handlers/ │ │ ├── index.ts # Handler exports (collectRequirements, preCheck, check, postCheck) │ │ ├── tazapay-create-entity.ts # Entity creation handler (KYB) │ │ ├── tazapay-verify-entity.ts # Entity verification handler │ │ ├── tazapay-create-customer.ts # Customer creation handler (payins) │ │ └── tazapay-create-beneficiary.ts # Beneficiary creation handler │ ├── types.ts # Server-side types │ └── webhook-module.ts # Webhook handling │ ├── money-movement/ # Money movement hooks │ ├── index.ts # Exports createTransaction │ └── transaction.ts # Transaction implementation │ └── dashboard/ # Dashboard components ├── index.ts # Dashboard module export └── handlers/ ├── index.ts # Handler exports ├── TazapayOnboarding.tsx # Onboarding UI component ├── tazapay-onboarding.ts # Onboarding server action └── tazapay-onboarding.schema.ts # Form schemas ``` --- ### File Details #### `manifest.ts` **Purpose**: Define plugin metadata, capabilities, and requirements **Key Exports**: `manifest: PluginManifest` ```typescript // Pseudocode export const manifest: PluginManifest = { id: 'TAZAPAY', version: '1.0.0', name: 'tazapay', description: 'Tazapay cross-border payment provider', requiredWebhookKeys: ['events'], webhookRegistrationMethod: WebhookRegistrationMethod.DASHBOARD, credentialSchema: { jsonSchema, uiSchema }, capabilities: [ // Onramp: Fiat -> USDC/USDT { src: 'USD', dest: 'USDC_ETH', priority: 100 }, { src: 'USD', dest: 'USDC_POLYGON', priority: 100 }, // ... all currency pairs // Offramp: USDC/USDT -> Fiat { src: 'USDC_ETH', dest: 'USD', priority: 100 }, // ... etc ], requirementsByEdgeType: { KYC_SERVICE_PROVIDER: [ { code: 'TAZAPAY_CREATE_ENTITY', description: 'Create Tazapay Entity' }, { code: 'TAZAPAY_VERIFY_ENTITY', description: 'Verify Entity KYB', dependencies: ['TAZAPAY_CREATE_ENTITY'] }, ], }, }; ``` --- #### `client/tazapay.api.service.ts` **Purpose**: HTTP client for Tazapay API **Key Exports**: `TazapayApiService` class ```typescript // Pseudocode for key methods class TazapayApiService { constructor(config: TazapayConfig) { ... } // Authentication: HTTP Basic Auth private _getAuthHeader(): string { return `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString('base64')}`; } // Entity (KYB) APIs async createEntity(request: EntityCreateRequest): Promise<EntityResponse> { ... } async getEntity(entityId: string): Promise<EntityResponse> { ... } async submitEntity(entityId: string): Promise<EntityResponse> { ... } // Customer APIs async createCustomer(request: CustomerCreateRequest): Promise<CustomerResponse> { ... } async getCustomer(customerId: string): Promise<CustomerResponse> { ... } // Beneficiary APIs async createBeneficiary(request: BeneficiaryCreateRequest): Promise<BeneficiaryResponse> { ... } async getBeneficiary(beneficiaryId: string): Promise<BeneficiaryResponse> { ... } // Payout APIs async createPayoutQuote(request: PayoutQuoteRequest): Promise<PayoutQuoteResponse> { ... } async createPayout(request: PayoutCreateRequest): Promise<PayoutResponse> { ... } async getPayout(payoutId: string): Promise<PayoutResponse> { ... } // Checkout/Payin APIs async createCheckout(request: CheckoutCreateRequest): Promise<CheckoutResponse> { ... } async getCheckout(checkoutId: string): Promise<CheckoutResponse> { ... } // Balance/FX APIs async getBalance(): Promise<BalanceResponse> { ... } async getFxQuote(request: FxQuoteRequest): Promise<FxQuoteResponse> { ... } // Collection Account APIs async getCollectionAccounts(): Promise<CollectionAccountResponse[]> { ... } } ``` --- #### `client/interfaces.ts` **Purpose**: TypeScript type definitions for all API interactions **Key Exports**: All request/response interfaces ```typescript // Key interfaces (see INSIGHTS.md Section 11 for full schemas) interface TazapayCredentials { apiKey, apiSecret, webhookSecret, baseUrl } interface EntityCreateRequest { name, type, email, ... } interface EntityResponse { id, approval_status, ... } interface BeneficiaryCreateRequest { name, type, destination_details, ... } interface PayoutQuoteRequest { payout_type, payout_info, ... } interface PayoutCreateRequest { amount, currency, beneficiary, ... } interface CheckoutCreateRequest { invoice_currency, amount, customer_details, ... } interface WebhookPayload { type, id, created_at, data } ``` --- #### `api-server/handlers/index.ts` **Purpose**: Dynamic handler loading and requirement collection **Key Exports**: `collectRequirements`, `preCheck`, `check`, `postCheck` ```typescript // Pseudocode export const collectRequirements = async (intent, edge, context) => { const requirements = manifest.requirementsByEdgeType?.[edge.type]; if (!requirements) return []; return requirements.map(req => ({ code: req.code, description: req.description, dependencies: req.dependencies, complianceEdge: { connect: { id: edge.id } }, compliancePlan: { connect: { id: intent.compliancePlan!.id } }, })); }; // Dynamic handler dispatch async function _executeHandler(phase, intent, requirement, context) { const fileName = requirementCodeToHandlerFileName(requirement.code); const functionName = phase + requirementCodeToHandlerFunctionSuffix(requirement.code); const handlerModule = await import(`./${fileName}.js`); const handler = handlerModule[functionName]; if (!handler) return intent; return handler(intent, requirement, context); } export const preCheck = (intent, req, ctx) => _executeHandler('preCheck', intent, req, ctx); export const check = (intent, req, ctx) => _executeHandler('check', intent, req, ctx); export const postCheck = (intent, req, ctx) => _executeHandler('postCheck', intent, req, ctx); ``` --- #### `api-server/handlers/tazapay-create-entity.ts` **Purpose**: Create Tazapay Entity for KYB **Key Exports**: `checkTazapayCreateEntity`, `postCheckTazapayCreateEntity` ```typescript // Pseudocode export async function checkTazapayCreateEntity(intent, requirement, context) { const credentials = await context.getCredentials(); const client = new TazapayApiService(credentials); // Check if entity already exists in CustomerProvider const customerProvider = await getCustomerProvider(prisma, customerId, 'tazapay'); if (customerProvider?.providerReferenceId) { // Entity exists, verify status const entity = await client.getEntity(customerProvider.providerReferenceId); if (entity.approval_status === 'approved') return intent; // Done if (entity.approval_status === 'rejected') throw new Error('KYB rejected'); } // Create new entity const entityRequest = buildEntityRequest(intent); const entity = await client.createEntity(entityRequest); // Store entity ID await upsertCustomerProvider(prisma, customerId, 'tazapay', { providerReferenceId: entity.id, onboardingData: { entityId: entity.id, kybStatus: entity.approval_status }, }); // Store in intent.metadata for downstream handlers await context.prisma.intent.update({ where: { id: intent.id }, data: { metadata: { ...intent.metadata, tazapay_entity_id: entity.id } }, }); return intent; } export async function postCheckTazapayCreateEntity(intent, requirement, context) { // Propagate entity ID to dependent steps const entityId = (requirement.metadata as any)?.entityId; // ...propagate to TAZAPAY_VERIFY_ENTITY step return intent; } ``` --- #### `api-server/handlers/tazapay-verify-entity.ts` **Purpose**: Verify Entity KYB status **Key Exports**: `checkTazapayVerifyEntity` ```typescript // Pseudocode export async function checkTazapayVerifyEntity(intent, requirement, context) { const credentials = await context.getCredentials(); const client = new TazapayApiService(credentials); const entityId = (intent.metadata as any)?.tazapay_entity_id; if (!entityId) throw new Error('Entity ID not found'); const entity = await client.getEntity(entityId); switch (entity.approval_status) { case 'approved': // Update CustomerProvider status await updateCustomerProviderById(prisma, customerProvider.id, { onboardingData: { kybStatus: 'approved', approvedAt: new Date().toISOString() }, }); return intent; case 'pending': throw new Error('KYB pending additional documents'); case 'rejected': throw new Error(`KYB rejected: ${entity.rejection_reason}`); case 'submitted': case 'initiated': throw new Error('KYB still under review'); default: throw new Error(`Unknown KYB status: ${entity.approval_status}`); } } ``` --- #### `api-server/webhook-module.ts` **Purpose**: Handle incoming webhooks from Tazapay **Key Exports**: `webhookModule: WebhookModule` ```typescript // Pseudocode export const webhookModule: WebhookModule = { async authenticate(authContext, pluginContext): Promise<string | null> { const { request } = authContext; const credentials = await pluginContext.getCredentials(); // Verify HMAC-SHA256 signature const payload = JSON.stringify(request.body); const signature = request.headers['x-tazapay-signature']; const eventId = request.body.id; const createdAt = request.body.created_at; if (!_verifySignature(payload, signature, credentials.webhookSecret, eventId, createdAt)) { return null; } // Check timestamp for replay attack if (!_isTimestampValid(createdAt, 10 * 60 * 1000)) return null; // Find intent by provider transaction ID const data = request.body.data; const intent = await _findIntentByProviderRef(pluginContext.prisma, data.id); return intent?.id ?? null; }, async handle(handleContext, pluginContext): Promise<void> { const { intentId, webhookEvent } = handleContext; const payload = webhookEvent.eventData; const eventType = payload.type; // Route by event type if (eventType.startsWith('payout.')) { await _handlePayoutWebhook(intentId, payload, pluginContext); } else if (eventType.startsWith('payin.')) { await _handlePayinWebhook(intentId, payload, pluginContext); } else if (eventType.startsWith('entity.')) { await _handleEntityWebhook(intentId, payload, pluginContext); } else if (eventType.startsWith('collect.')) { await _handleCollectWebhook(intentId, payload, pluginContext); } }, }; // Helper: HMAC-SHA256 verification per Tazapay docs function _verifySignature(payload, signature, secret, eventId, createdAt): boolean { const signatureData = `${eventId}${payload}${createdAt}`; const expected = crypto.createHmac('sha256', secret).update(signatureData).digest('base64'); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } ``` --- #### `money-movement/transaction.ts` **Purpose**: Execute onramp and offramp transactions **Key Exports**: `createTransaction` ```typescript // Pseudocode export async function createTransaction( context: PluginContext, transactionContext: TransactionContext, ): Promise<PluginTransactionResult> { const credentials = await context.getCredentials(); const client = new TazapayApiService(credentials); const { intent, externalAccounts, amount } = transactionContext; const isOfframp = externalAccounts.src.type === ExternalAccountType.CRYPTO_ADDRESS; // Check for existing transaction (idempotency) const existingTxnId = (intent.metadata as any)?.tazapay_payout_id || (intent.metadata as any)?.tazapay_checkout_id; if (existingTxnId) { return _handleExistingTransaction(client, existingTxnId, isOfframp); } if (isOfframp) { return _handleOfframp(client, context, transactionContext); } else { return _handleOnramp(client, context, transactionContext); } } async function _handleOfframp(client, context, transactionContext): Promise<PluginTransactionResult> { const { intent, externalAccounts, amount } = transactionContext; // 1. Get or create beneficiary const beneficiaryId = await _getOrCreateBeneficiary(client, context, externalAccounts.dest); // 2. Create payout quote const quote = await client.createPayoutQuote({ payout_type: _determinePayoutType(externalAccounts.dest), payout_info: { currency: amount.currency.code, amount: _toCents(amount.amount) }, holding_info: { currency: _getHoldingCurrency(externalAccounts.src) }, }); // 3. Create payout const payout = await client.createPayout({ amount: _toCents(amount.amount), currency: amount.currency.code, beneficiary: beneficiaryId, quote: quote.id, purpose: 'PYR001', // Generic purpose code transaction_description: `Offramp ${intent.id}`, reference_id: intent.id, }); // 4. Store provider reference await context.prisma.intent.update({ where: { id: intent.id }, data: { metadata: { ...(intent.metadata || {}), tazapay_payout_id: payout.id, tazapay_beneficiary_id: beneficiaryId } }, }); // 5. Return deposit instructions for crypto return { status: MoneyMovementTransactionStatus.IN_PROGRESS, depositInstructions: _buildCryptoDepositInstructions(externalAccounts.src, await _getCollectionWallet(client)), }; } async function _handleOnramp(client, context, transactionContext): Promise<PluginTransactionResult> { const { intent, externalAccounts, amount } = transactionContext; // 1. Create checkout session const checkout = await client.createCheckout({ invoice_currency: amount.currency.code, amount: _toCents(amount.amount), customer_details: _buildCustomerDetails(intent), success_url: `${process.env.APP_URL}/checkout/success?intent=${intent.id}`, cancel_url: `${process.env.APP_URL}/checkout/cancel?intent=${intent.id}`, transaction_description: `Onramp ${intent.id}`, reference_id: intent.id, }); // 2. Store provider reference await context.prisma.intent.update({ where: { id: intent.id }, data: { metadata: { ...(intent.metadata || {}), tazapay_checkout_id: checkout.id, tazapay_checkout_url: checkout.url } }, }); // 3. Return hosted checkout instructions return { status: MoneyMovementTransactionStatus.IN_PROGRESS, depositInstructions: [{ kind: 'HOSTED', label: `Complete payment via Tazapay`, hostedUrl: checkout.url, metadata: { checkoutId: checkout.id, expiresAt: checkout.expires_at }, }], }; } ``` --- ## === SEQUENCE DIAGRAMS === ### 2. Flow Diagrams #### a) Onboarding Flow (KYB) ```mermaid sequenceDiagram participant User participant Dashboard participant API Server participant Tazapay API participant Webhook User->>Dashboard: Start onboarding Dashboard->>API Server: Create Intent API Server->>API Server: collectRequirements() Note over API Server: Returns TAZAPAY_CREATE_ENTITY, TAZAPAY_VERIFY_ENTITY API Server->>API Server: check(TAZAPAY_CREATE_ENTITY) API Server->>Tazapay API: POST /v3/entity Tazapay API-->>API Server: { id: "ent_xxx", approval_status: "initiated" } API Server->>API Server: Store entityId in CustomerProvider API Server->>API Server: Update intent.metadata API Server->>Tazapay API: POST /v3/entity/{id}/submit Tazapay API-->>API Server: { approval_status: "submitted" } Note over Tazapay API: Async KYB review... Tazapay API->>Webhook: POST entity.approved Webhook->>API Server: webhookModule.authenticate() Webhook->>API Server: webhookModule.handle() API Server->>API Server: Update CustomerProvider.onboardingData API Server->>API Server: check(TAZAPAY_VERIFY_ENTITY) API Server->>Tazapay API: GET /v3/entity/{id} Tazapay API-->>API Server: { approval_status: "approved" } API Server-->>Dashboard: KYB Complete Dashboard-->>User: Onboarding complete ``` #### b) Quote Flow ```mermaid sequenceDiagram participant User participant API Server participant Tazapay API User->>API Server: Request quote (100 USD -> EUR bank) API Server->>Tazapay API: POST /v3/payout/quote Note over API Server,Tazapay API: { payout_type: "local", payout_info: { currency: "USD", amount: 10000 }, destination_info: { currency: "EUR" } } Tazapay API-->>API Server: PayoutQuoteResponse Note over Tazapay API,API Server: { id: "poq_xxx", holding_info: { amount: 10000 }, destination_info: { amount: 9200 }, fee_info: { ... }, exchange_rates: { ... }, valid_until: "..." } API Server->>API Server: Store quote details API Server-->>User: Quote response Note over User: Quote valid ~30 min ``` #### c) Transaction Flow (Offramp: Crypto -> Fiat) ```mermaid sequenceDiagram participant User participant API Server participant Tazapay API participant Blockchain participant Webhook User->>API Server: Confirm offramp (USDC -> USD bank) API Server->>API Server: createTransaction() API Server->>Tazapay API: POST /v3/beneficiary (if needed) Tazapay API-->>API Server: { id: "bnf_xxx" } API Server->>Tazapay API: POST /v3/payout/quote Tazapay API-->>API Server: { id: "poq_xxx", valid_until: "..." } API Server->>Tazapay API: POST /v3/payout Note over API Server,Tazapay API: { amount, currency, beneficiary: "bnf_xxx", quote: "poq_xxx", ... } Tazapay API-->>API Server: { id: "pot_xxx", status: "requires_funding" } API Server->>API Server: Store pot_xxx in intent.metadata API Server-->>User: Deposit instructions (collection wallet address) User->>Blockchain: Send USDC to collection wallet Blockchain-->>Tazapay API: Funds received Tazapay API->>Webhook: POST collect.succeeded Webhook->>API Server: Update transaction status Tazapay API->>Webhook: POST payout.processing Webhook->>API Server: Status: IN_PROGRESS Note over Tazapay API: Process payout... Tazapay API->>Webhook: POST payout.succeeded Webhook->>API Server: Status: SUCCESS API Server->>API Server: Update MoneyMovementTransaction API Server-->>User: Transaction complete ``` #### d) Transaction Flow (Onramp: Fiat -> Crypto) ```mermaid sequenceDiagram participant User participant Dashboard participant API Server participant Tazapay API participant Hosted Checkout participant Webhook User->>API Server: Initiate onramp (USD card -> USDC) API Server->>API Server: createTransaction() API Server->>Tazapay API: POST /v3/checkout Note over API Server,Tazapay API: { invoice_currency: "USD", amount: 10000, customer_details: {...}, success_url, cancel_url } Tazapay API-->>API Server: { id: "chk_xxx", url: "https://checkout.tazapay.com/..." } API Server->>API Server: Store chk_xxx in intent.metadata API Server-->>Dashboard: Redirect to hosted checkout Dashboard-->>User: Show checkout link User->>Hosted Checkout: Complete payment Hosted Checkout-->>User: Redirect to success_url Tazapay API->>Webhook: POST payin.succeeded Webhook->>API Server: webhookModule.handle() API Server->>API Server: Funds in balance Note over API Server: Trigger payout to user's wallet API Server->>Tazapay API: POST /v3/payout (type: wallet) Tazapay API-->>API Server: { id: "pot_xxx" } Tazapay API->>Webhook: POST payout.succeeded Webhook->>API Server: Status: SUCCESS API Server-->>User: USDC received ``` #### e) Webhook Processing Flow ```mermaid sequenceDiagram participant Tazapay participant Webhook Endpoint participant webhookModule participant Database participant Temporal Tazapay->>Webhook Endpoint: POST /webhook/tazapay/:projectId/:webhookKey Note over Tazapay,Webhook Endpoint: Headers: x-tazapay-signature<br>Body: { type: "payout.succeeded", id: "evt_xxx", created_at, data: {...} } Webhook Endpoint->>webhookModule: authenticate(request) webhookModule->>webhookModule: Verify HMAC-SHA256 signature Note over webhookModule: signature_data = event_id + payload + created_at<br>expected = HMAC-SHA256(secret, signature_data) webhookModule->>webhookModule: Check timestamp (< 10 min) webhookModule->>Database: Find intent by provider reference Note over webhookModule,Database: Query intent.metadata.tazapay_payout_id webhookModule-->>Webhook Endpoint: Return intentId Webhook Endpoint->>Database: Create WebhookEvent record Webhook Endpoint-->>Tazapay: 200 OK Note over Webhook Endpoint: Async processing begins Webhook Endpoint->>webhookModule: handle(intentId, webhookEvent) webhookModule->>Database: Update MoneyMovementTransaction status Note over webhookModule,Database: payout.succeeded -> SUCCESS<br>payout.failed -> FAILED webhookModule->>Database: Update intent.metadata with tracking details webhookModule->>Database: Mark WebhookEvent as COMPLETED webhookModule->>Temporal: triggerCheckOnce(intentId) Note over Temporal: Workflow advancement ``` --- ## === DATA MODEL DESIGN === ### 3. Persistence Schema #### a) CustomerProvider.onboardingData Structure ```typescript interface TazapayOnboardingData { // Entity (KYB) tracking entityId?: string; // ent_xxx - Tazapay entity ID entityType?: 'individual' | 'sole_proprietorship' | 'company' | 'non_profit' | 'government_entity'; kybStatus?: 'initiated' | 'submitted' | 'pending' | 'approved' | 'rejected'; kybSubmittedAt?: string; // ISO 8601 timestamp kybApprovedAt?: string; // ISO 8601 timestamp kybRejectedAt?: string; // ISO 8601 timestamp kybRejectionReason?: string; // Reason if rejected pendingDocuments?: string[]; // Document types still needed // Customer (payin) tracking customerId?: string; // cus_xxx - Tazapay customer ID // Beneficiary cache beneficiaries?: { id: string; // bnf_xxx type: 'bank' | 'wallet' | 'local_payment_network'; currency: string; // e.g., "USD", "EUR" destinationType: string; // e.g., "swift", "local", "upi" destinationId: string; // bnk_xxx or wal_xxx createdAt: string; isVerified: boolean; }[]; // Collection accounts collectionAccounts?: { id: string; // cva_xxx paymentMethodType: string; // e.g., "stablecoin_usdc" currency: string; chain?: string; // For crypto: ethereum, polygon, etc. depositAddress?: string; // For crypto collection virtualAccount?: { accountNumber: string; bankName: string; routingCodes: Record<string, string>; }; createdAt: string; }[]; // Last webhook event tracking lastWebhookEventId?: string; lastWebhookEventAt?: string; } ``` #### b) Intent.metadata Fields **Fields READ from intent.metadata:** ```typescript interface TazapayIntentMetadataRead { // Customer info (populated by uRamp) customer_email?: string; customer_name?: string; customer_country?: string; customer_phone?: { calling_code: string; number: string }; // Destination account details destination_bank?: { account_number: string; bank_codes: Record<string, string>; country: string; currency: string; }; destination_wallet?: { address: string; chain: string; currency: string; }; // Source details for onramp source_payment_method?: string; // card, bank_transfer, etc. } ``` **Fields WRITTEN to intent.metadata:** ```typescript interface TazapayIntentMetadataWrite { // Entity tracking tazapay_entity_id?: string; // ent_xxx // Customer tracking tazapay_customer_id?: string; // cus_xxx // Beneficiary tracking tazapay_beneficiary_id?: string; // bnf_xxx // Transaction tracking - Payout (offramp) tazapay_payout_id?: string; // pot_xxx tazapay_payout_quote_id?: string; // poq_xxx tazapay_payout_status?: string; // Provider status // Transaction tracking - Checkout (onramp) tazapay_checkout_id?: string; // chk_xxx tazapay_checkout_url?: string; // Hosted checkout URL tazapay_payin_id?: string; // pay_xxx (linked to checkout) // Collection tracking tazapay_collect_id?: string; // col_xxx tazapay_collection_account_id?: string; // FX tracking tazapay_fx_quote_id?: string; // fx_xxx tazapay_exchange_rate?: number; // Tracking/settlement info tazapay_tracking_number?: string; tazapay_tracking_type?: 'uetr' | 'utr' | 'transaction_hash' | 'pix_id'; // Completion data tazapay_completed_at?: string; tazapay_settlement_date?: string; tazapay_fee_amount?: number; tazapay_fee_currency?: string; } ``` #### c) CompliancePlanStep.metadata Fields ```typescript interface TazapayStepMetadata { // For TAZAPAY_CREATE_ENTITY step entityId?: string; entityType?: string; submittedAt?: string; // For TAZAPAY_VERIFY_ENTITY step verificationAttempts?: number; lastVerificationAt?: string; approvalStatus?: string; // For TAZAPAY_CREATE_CUSTOMER step customerId?: string; // For TAZAPAY_CREATE_BENEFICIARY step beneficiaryId?: string; beneficiaryType?: 'bank' | 'wallet' | 'local_payment_network'; isVerificationRequired?: boolean; verificationStatus?: string; // Error tracking lastError?: string; errorCode?: string; retryCount?: number; } ``` #### d) MoneyMovementTransaction.context Fields ```typescript interface TazapayTransactionContext { // Payout tracking payoutId?: string; // pot_xxx payoutQuoteId?: string; // poq_xxx payoutType?: 'local' | 'swift' | 'wallet' | 'local_payment_network'; payoutStatus?: string; // Provider status string // Payin tracking payinId?: string; // pay_xxx checkoutId?: string; // chk_xxx checkoutUrl?: string; paymentMethod?: string; // card, bank_transfer, etc. // Collect tracking collectId?: string; // col_xxx collectionAccountId?: string; // cva_xxx // Beneficiary info beneficiaryId?: string; // bnf_xxx beneficiaryType?: 'bank' | 'wallet' | 'local_payment_network'; // FX info fxQuoteId?: string; // fx_xxx holdingCurrency?: string; destinationCurrency?: string; exchangeRate?: number; // Fee breakdown fees?: { fixed?: { amount: number; currency: string }; variable?: { percentage: number; amount: number; currency: string }; total?: number; }; // Tracking details trackingDetails?: { trackingNumber: string; trackingType: 'uetr' | 'utr' | 'transaction_hash' | 'pix_id'; }; // Balance info balanceTransactionId?: string; // btr_xxx availableBalance?: number; isBalanceSufficient?: boolean; // Timestamps providerCreatedAt?: string; providerCompletedAt?: string; settlementDate?: string; // Error info (if failed) failureReason?: string; failureCode?: string; isRetryable?: boolean; } ``` --- ## === EDGE CASES & ERROR HANDLING === ### 4. Edge Case Catalog | Scenario | Detection | Handling | Recovery | |----------|-----------|----------|----------| | **API timeout** | HTTP timeout (>30s) | Retry with exponential backoff | Max 3 retries, then fail with PROVIDER_TIMEOUT | | **Rate limited (429)** | HTTP 429 response | Exponential backoff starting at 1s | Max delay 30s, max 5 retries | | **Server error (5xx)** | HTTP 500/502/503/504 | Retry with backoff | Max 3 retries, log for ops investigation | | **Invalid credentials** | HTTP 401 | No retry, fail immediately | Alert ops, require credential update | | **Insufficient permissions** | HTTP 403 | No retry | Check API key permissions in dashboard | | **Entity not found** | HTTP 404 on GET /v3/entity | Check CustomerProvider for stale ID | Re-create entity if needed | | **Duplicate entity** | Error code on POST /v3/entity | Extract existing entity ID from error | Use existing entity, update CustomerProvider | | **KYB rejected** | `entity.rejected` webhook or approval_status | Mark step failed, notify user | Manual review, allow re-submission | | **KYB pending docs** | `entity.pending` webhook or approval_status | Mark step ACTION_REQUIRED | Prompt user for additional documents | | **Quote expired** | HTTP 422 with code 20299 | Create new quote | Auto-refresh quote on retry | | **Insufficient balance** | Error code 20199 or `requires_funding` status | Wait for deposit | Poll collection account, retry payout | | **Amount outside limits** | Error code 20298 | Fail with clear message | Inform user of min/max limits | | **Invalid bank details** | Error codes 3524, 3529 | Fail with validation error | Prompt user to correct details | | **Beneficiary verification required** | `is_doc_verification_required: true` | Mark step ACTION_REQUIRED | Prompt user for wallet verification docs | | **Webhook signature invalid** | HMAC mismatch | Reject webhook, log warning | Alert ops if persistent | | **Webhook replay attack** | Timestamp > 10 minutes old | Reject webhook | Log for investigation | | **Duplicate webhook** | Same event_id already processed | Idempotent skip | Return 200 OK, no action | | **Transaction already exists** | provider_transaction_id in intent.metadata | Return existing transaction status | Idempotent response | | **Payout reversed** | `payout.reversed` webhook | Mark FAILED, store reason | Manual ops intervention | | **Partial completion** | N/A (Tazapay doesn't support partial) | Full amount or failure | Standard retry/fail flow | | **Collection wallet mismatch** | Deposit to wrong address | Cannot auto-detect | Manual reconciliation | | **Wrong amount deposited** | Amount differs from expected | Depends on provider handling | May need manual intervention | ### 5. Idempotency Strategy #### Transaction Creation Idempotency ```typescript // Before creating any provider resource, check if it already exists async function createTransaction(...) { // Check for existing payout const existingPayoutId = (intent.metadata as any)?.tazapay_payout_id; if (existingPayoutId) { const payout = await client.getPayout(existingPayoutId); return _mapPayoutToResult(payout); } // Check for existing checkout const existingCheckoutId = (intent.metadata as any)?.tazapay_checkout_id; if (existingCheckoutId) { const checkout = await client.getCheckout(existingCheckoutId); return _mapCheckoutToResult(checkout); } // Create new transaction... } ``` #### API Request Idempotency ```typescript // Use Idempotency-Key header for all POST requests async function _post<T>(endpoint: string, body: any, idempotencyKey?: string): Promise<T> { const headers = { 'Authorization': this._getAuthHeader(), 'Content-Type': 'application/json', ...(idempotencyKey && { 'Idempotency-Key': idempotencyKey }), }; return this._request('POST', endpoint, body, headers); } // Generate idempotency keys from intent + operation function _generateIdempotencyKey(intentId: string, operation: string): string { return `${intentId}-${operation}-${Date.now()}`.slice(0, 255); } // Usage in createPayout const idempotencyKey = _generateIdempotencyKey(intent.id, 'create-payout'); const payout = await client.createPayout(request, idempotencyKey); ``` #### Webhook Idempotency ```typescript async function handle(handleContext, pluginContext): Promise<void> { const { webhookEvent } = handleContext; const eventId = webhookEvent.eventData?.id; // Check if already processed (Tazapay event IDs are unique) const existing = await pluginContext.prisma.webhookEvent.findFirst({ where: { eventType: { startsWith: 'tazapay.' }, eventData: { path: ['id'], equals: eventId }, status: 'COMPLETED', }, }); if (existing) { pluginContext.logger.info('Duplicate webhook event, skipping', { eventId }); return; } // Process webhook... } ``` #### Retry Safety | Operation | Idempotency Method | Key Format | |-----------|-------------------|------------| | Create Entity | Check CustomerProvider.providerReferenceId | N/A (check before create) | | Create Beneficiary | Check intent.metadata.tazapay_beneficiary_id | N/A (check before create) | | Create Payout | Idempotency-Key header | `{intentId}-payout-{timestamp}` | | Create Checkout | Idempotency-Key header | `{intentId}-checkout-{timestamp}` | | Create Quote | No idempotency needed | N/A (quotes are cheap, short-lived) | | Webhook Handle | Check webhookEvent by eventId | Provider event ID | --- ## === TEST SPECIFICATIONS === ### 6. Mock Server Test Scenarios #### Endpoints to Mock | Endpoint | Method | Mock Scenarios | |----------|--------|----------------| | `/v3/entity` | POST | Success (201), Duplicate (409), Validation error (422) | | `/v3/entity/{id}` | GET | Found (200), Not found (404) | | `/v3/entity/{id}/submit` | POST | Success (200), Already submitted (400) | | `/v3/customer` | POST | Success (201), Duplicate (409) | | `/v3/customer/{id}` | GET | Found (200), Not found (404) | | `/v3/beneficiary` | POST | Success (201), Validation error (422), Verification required | | `/v3/beneficiary/{id}` | GET | Found (200), Not found (404) | | `/v3/payout/quote` | POST | Success (200), Invalid params (400), Amount limits (422) | | `/v3/payout` | POST | Success (201), Quote expired (422), Insufficient balance (422) | | `/v3/payout/{id}` | GET | All status variants | | `/v3/checkout` | POST | Success (201), Validation error (422) | | `/v3/checkout/{id}` | GET | All status variants | | `/v3/balance` | GET | Success with balances (200) | | `/v3/collection_account` | GET | Success with accounts (200) | #### Mock Response Examples ```typescript // mock-server/fixtures/entity-responses.ts export const entityApproved = { status: 'success', data: { id: 'ent_test_123', name: 'Test Company', type: 'company', approval_status: 'approved', created_at: '2024-01-01T00:00:00Z', }, }; export const entityPending = { status: 'success', data: { id: 'ent_test_456', approval_status: 'pending', pending_requirements: ['business_registration', 'id_document'], }, }; export const payoutSucceeded = { id: 'pot_test_789', object: 'payout', amount: 10000, currency: 'USD', status: 'succeeded', beneficiary: 'bnf_test_001', tracking_details: { tracking_number: 'UETR123456', tracking_type: 'uetr', }, created_at: '2024-01-01T00:00:00Z', }; ``` ### 7. Plugin Test Scenarios #### Unit Tests per Handler **tazapay-create-entity.ts** - [ ] Creates entity when CustomerProvider doesn't exist - [ ] Returns existing entity when CustomerProvider has entityId and status is approved - [ ] Throws error when existing entity is rejected - [ ] Handles API timeout with retry - [ ] Handles duplicate entity error gracefully - [ ] Stores entityId in intent.metadata and CustomerProvider **tazapay-verify-entity.ts** - [ ] Returns success when entity is approved - [ ] Throws error when entity is pending (ACTION_REQUIRED) - [ ] Throws error when entity is rejected with reason - [ ] Throws error when entity is still under review - [ ] Updates CustomerProvider.onboardingData on status change **webhook-module.ts (authenticate)** - [ ] Returns intentId for valid signature - [ ] Returns null for invalid signature - [ ] Returns null for missing signature header - [ ] Returns null for expired timestamp (>10 min) - [ ] Returns null when intent not found **webhook-module.ts (handle)** - [ ] Updates MoneyMovementTransaction to SUCCESS on payout.succeeded - [ ] Updates MoneyMovementTransaction to FAILED on payout.failed - [ ] Updates CustomerProvider.onboardingData on entity.approved - [ ] Handles collect.succeeded and triggers payout - [ ] Skips duplicate events (idempotent) **transaction.ts (offramp)** - [ ] Creates beneficiary if not exists - [ ] Reuses existing beneficiary from intent.metadata - [ ] Creates quote and payout - [ ] Returns crypto deposit instructions - [ ] Handles quote expiry by creating new quote - [ ] Handles insufficient balance gracefully - [ ] Idempotent: returns existing payout if already created **transaction.ts (onramp)** - [ ] Creates checkout session - [ ] Returns hosted checkout URL - [ ] Handles checkout expiry - [ ] Idempotent: returns existing checkout if already created #### Integration Tests for Full Flows **Offramp Flow (USDC -> USD bank)** 1. Create intent with crypto source, bank destination 2. Run compliance flow (entity creation, verification) 3. Execute createTransaction 4. Verify deposit instructions returned 5. Simulate collect.succeeded webhook 6. Simulate payout.succeeded webhook 7. Verify MoneyMovementTransaction status is SUCCESS **Onramp Flow (USD card -> USDC wallet)** 1. Create intent with fiat source, crypto destination 2. Run compliance flow (customer creation) 3. Execute createTransaction 4. Verify checkout URL returned 5. Simulate payin.succeeded webhook 6. Verify wallet payout triggered 7. Simulate payout.succeeded webhook 8. Verify MoneyMovementTransaction status is SUCCESS **KYB Rejection Flow** 1. Create intent 2. Run compliance flow 3. Simulate entity.rejected webhook 4. Verify step marked as FAILED 5. Verify CustomerProvider.onboardingData updated with rejection reason --- ## === IMPLEMENTATION ORDER === ### 8. Dependency Graph ``` ┌──────────────────┐ │ types.ts │ │ interfaces.ts │ └────────┬─────────┘ │ ┌────────▼─────────┐ │ config.ts │ │credential.schema │ └────────┬─────────┘ │ ┌────────▼─────────┐ │ tazapay.api. │ │ service.ts │ └────────┬─────────┘ │ ┌──────────────┼──────────────┐ │ │ │ ┌────────▼───────┐ ┌────▼────┐ ┌───────▼───────┐ │ handlers/ │ │manifest │ │ webhook- │ │ *.ts │ │ .ts │ │ module.ts │ └────────┬───────┘ └────┬────┘ └───────┬───────┘ │ │ │ └──────────────┼──────────────┘ │ ┌────────▼─────────┐ │ transaction.ts │ │ money-movement/ │ └────────┬─────────┘ │ ┌────────▼─────────┐ │ dashboard/ │ │ components │ └────────┬─────────┘ │ ┌────────▼─────────┐ │ index.ts │ │ (main export) │ └──────────────────┘ ``` ### Implementation Phases #### Phase 1: Foundation (client layer) 1. `client/interfaces.ts` - All TypeScript types 2. `client/config.ts` - Constants, base URLs 3. `client/credential.schema.ts` - Credential form 4. `client/tazapay.api.service.ts` - API client #### Phase 2: Compliance (server handlers) 5. `api-server/types.ts` - Server-side types 6. `api-server/handlers/index.ts` - Handler infrastructure 7. `api-server/handlers/tazapay-create-entity.ts` 8. `api-server/handlers/tazapay-verify-entity.ts` 9. `api-server/handlers/tazapay-create-customer.ts` 10. `api-server/handlers/tazapay-create-beneficiary.ts` #### Phase 3: Webhooks 11. `api-server/webhook-module.ts` - Full webhook handling #### Phase 4: Money Movement 12. `money-movement/index.ts` - Capability matching 13. `money-movement/transaction.ts` - Transaction execution #### Phase 5: Integration 14. `manifest.ts` - Plugin manifest 15. `index.ts` - Main exports #### Phase 6: Dashboard (optional, can be deferred) 16. `dashboard/handlers/index.ts` 17. `dashboard/handlers/TazapayOnboarding.tsx` 18. `dashboard/handlers/tazapay-onboarding.ts` 19. `dashboard/index.ts` #### Phase 7: Testing 20. Mock server setup 21. Unit tests 22. Integration tests --- ## Appendix: Currency Pairs to Implement Based on USER_INPUT.md, the following currency pairs will be implemented: ### Onramp (Fiat -> Crypto) | Source (Fiat) | Destination (Crypto) | Transfer Type | |---------------|---------------------|---------------| | USD | USDC_ETH, USDC_POLYGON, USDC_TRON, USDC_SOLANA | Card, ACH | | USD | USDT_ETH, USDT_POLYGON, USDT_TRON, USDT_SOLANA | Card, ACH | | SGD | USDC_*, USDT_* | Card, PayNow | | EUR | USDC_*, USDT_* | Card, SEPA | | INR | USDC_*, USDT_* | Card, UPI, IMPS | | BRL | USDC_*, USDT_* | PIX | | PHP | USDC_*, USDT_* | InstaPay | | THB | USDC_*, USDT_* | PromptPay | | MXN | USDC_*, USDT_* | SPEI | | VND | USDC_*, USDT_* | Bank push | ### Offramp (Crypto -> Fiat) | Source (Crypto) | Destination (Fiat) | Transfer Type | |-----------------|-------------------|---------------| | USDC_ETH, USDC_POLYGON, USDC_TRON, USDC_SOLANA | USD | ACH, SWIFT | | USDT_ETH, USDT_POLYGON, USDT_TRON, USDT_SOLANA | USD | ACH, SWIFT | | USDC_*, USDT_* | SGD | FAST, SWIFT | | USDC_*, USDT_* | EUR | SEPA, SWIFT | | USDC_*, USDT_* | INR | RTGS, IMPS | | USDC_*, USDT_* | BRL | PIX | | USDC_*, USDT_* | PHP | InstaPay, PESONet | | USDC_*, USDT_* | THB | PromptPay | | USDC_*, USDT_* | MXN | SPEI | | USDC_*, USDT_* | VND | NAPAS | Total: ~80 currency pair combinations (9 fiat x 8 crypto + 8 crypto x 9 fiat) --- PHASE_3_COMPLETE

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully