Skip to content

feat: payroll escrow reclaim, webhook incident alerts, dunning workflow builder, multi-asset checkout - #192

Merged
Prudentdev-xyz merged 6 commits into
Orbit-xyz:mainfrom
Akanimoh12:feat/188-189-190-191-escrow-alerts-dunning-swap
Oct 18, 2026
Merged

Prudentdev-xyz merged 6 commits into
Orbit-xyz:mainfrom
Akanimoh12:feat/188-189-190-191-escrow-alerts-dunning-swap

Conversation

@Akanimoh12

Copy link
Copy Markdown
Contributor

Summary

This PR implements four issues in one branch:

Each change is its own commit, so they can be reviewed separately.

#188 Payroll escrow reclamation (contracts/soroban)

  • create_payroll_escrow pulls the deposit into the contract and records total_deposited, claimed_amount, expiration_timestamp, and a reclaimed flag.
  • claim_payroll_escrow lets the recipient withdraw while now <= expiration_timestamp.
  • reclaim_unclaimed_payroll(sender, payroll_id) requires sender auth and now > expiration_timestamp. It computes unclaimed = total_deposited - claimed_amount, asserts it is positive, marks the escrow reclaimed before transferring, and emits payroll_escrow_reclaimed.
  • Claims and reclaims meet at the expiration with no gap or overlap. The existing batch payroll and multisig flows are unchanged.

#189 Webhook incident alerting (apps/backend)

  • webhook-alerts.js tracks each endpoint over a 15-minute rolling window and reports the failure rate. An incident opens after 5 consecutive 5xx or connection errors. The next 2xx resolves it and sends a "Webhook Delivery Restored" notice. Other 4xx responses are neutral.
  • Each alert carries the endpoint URL, the failed event type, the error code, and a link to the DLQ inspector.
  • Channels: Slack Incoming Webhook, PagerDuty Events API v2 (trigger and resolve share a dedup_key), and email through a relay set by ALERT_EMAIL_RELAY_URL.
  • Routes: GET/POST /webhook-alert-channels, DELETE /webhook-alert-channels/:id. Backed by the new webhook_alert_channels table.
  • webhooks.js calls an optional observer after every dispatch and resend. It runs off the request path, so alert delivery never slows or fails a webhook.

#190 Dunning workflow builder (apps/backend + apps/frontend)

  • Backend: GET/PUT /dunning/workflow store a merchant's steps (day, retry toggle, action, grace discount, markdown templates) in dunning_workflows. Validation requires increasing days, a final attempt as the last step, and known placeholders only.
  • The scheduler honors a saved workflow for retry days and attempt count. Without one, the original Day 1 / 3 / 5 / 7 behavior is unchanged.
  • dunning.attempt_failed webhooks include the rendered email subject and body for the step, so the merchant can send it.
  • GET /dunning/analytics returns failed and recovered invoices, recovered volume, net churn prevented, and recovery rate.
  • Frontend: a new page at /dashboard/dunning with a timeline editor, a markdown editor with placeholder chips and a live preview (escaped before rendering), validation before save, and funnel cards.

#191 Multi-asset checkout (packages/checkout-widget + contract)

  • Pay With selector listing the connected wallet's USDC, EURC, XLM, and AQUA balances. Assets with no balance are hidden.
  • Live quote from Horizon strict-receive paths: rate, price impact against the mid-market rate (flagged above 5%), and the maximum input after slippage (0.5%, 1%, 3%).
  • Atomic swap and subscribe: a new contract entrypoint, create_vault_with_swap, creates the vault, stores the swap config, and approves the payment-token allowance in one invocation. Soroban allows one contract invocation per transaction, so this is what keeps the steps atomic. The whole call reverts if any step fails.
  • Router allowlist: merchants approve routers on-chain with approve_swap_router / revoke_swap_router. The contract rejects any router the merchant has not approved.

Testing

Run and passing:

  • Backend, Vitest: test/webhook-alerts.test.js, test/webhooks.test.js, test/dunning.test.js, 22 tests passing. This run was before the final dunning.js change, so the dunning suite should be re-run.

Written but not run:

  • Contract: new tests in contracts/soroban/src/test.rs for escrow (test_payroll_*) and swap subscribe (test_create_vault_with_swap_*). I have not compiled or run them. Run cargo test in contracts/soroban before merging.
  • Frontend: test/dunning-workflow.test.mjs (Node test runner) has not been run. The view has not been type-checked.
  • Widget: not built. The widget's package-lock.json is out of sync with its package.json (npm ci fails on utf-8-validate) and that predates this PR. The JSX was not syntax-checked.

Known limitations and follow-ups

Closes #188
Closes #189
Closes #190
Closes #191

Payroll deposits now live in contract escrow with an expiration_timestamp.
Recipients can claim until the deadline; once it passes, the sender can
reclaim whatever was never claimed.

- create_payroll_escrow: pulls the deposit into the contract and records
  total_deposited, expiration_timestamp, and an escrow id
- claim_payroll_escrow: recipient withdrawals, only while now <= expiration
- reclaim_unclaimed_payroll: sender-only, only while now > expiration;
  marks the escrow reclaimed before transferring unclaimed =
  total_deposited - claimed_amount, so it can only happen once
- emits payroll_escrow_created, payroll_escrow_claimed and
  payroll_escrow_reclaimed events
- get_payroll_escrow read-only lookup

Refs Orbit-xyz#188
… email

Tracks delivery health per merchant endpoint and alerts merchants when an
endpoint starts failing, then sends a recovery notice when it comes back.

- webhook-alerts.js: in-memory failure telemetry with a 15-minute rolling
  failure rate. An incident opens after 5 consecutive 5xx or connection
  errors and resolves on the next 2xx.
- Alerts carry endpoint URL, failed event type, error code and a link to
  the DLQ inspector.
- Channels: Slack Incoming Webhook, PagerDuty Events API v2 (trigger and
  resolve share a dedup key), and email via ALERT_EMAIL_RELAY_URL.
- Merchant routes: GET/POST /webhook-alert-channels and
  DELETE /webhook-alert-channels/:id, backed by a new
  webhook_alert_channels table.
- webhooks.js: optional delivery observer, called off the request path
  after every dispatch and resend. Delivery results are unchanged.

Refs Orbit-xyz#189
…bscribe

Subscribers can pay in an asset other than the plan currency. Merchants
approve the routers allowed to convert for them.

- approve_swap_router / revoke_swap_router: merchant-controlled allowlist
- create_vault_with_swap: one invocation that creates the vault, stores the
  swap config, and approves the payment-token allowance. Soroban allows a
  single invocation per transaction, so this is what keeps subscribe and
  swap atomic. It reverts as a whole on any failure and rejects routers the
  merchant has not approved.

Refs Orbit-xyz#191
…ery funnel

Merchants can now define their own recovery schedule for failed payments.

- dunning-workflow.js: step schema (day, retry pull, action, grace discount,
  markdown email templates), sequence validation (strictly increasing days,
  final attempt last, known placeholders only), and placeholder rendering.
- GET/PUT /dunning/workflow store the workflow in dunning_workflows. Without a
  saved workflow the default Day 1 / 3 / 5 / 7 cadence applies.
- processDunningRetries and markSubscriptionPastDue honor a saved workflow for
  retry days and attempt count. The default path is unchanged.
- dunning.attempt_failed webhooks include the rendered email for the step.
- GET /dunning/analytics: failed and recovered invoices, recovered volume,
  net churn prevented, and recovery rate.

Refs Orbit-xyz#190
Adds a dashboard page at /dashboard/dunning for configuring recovery steps.

- Timeline of steps (day, action, retry toggle, grace discount) with add,
  remove and select
- Step editor with a markdown email body, placeholder chips, and a live
  preview. The preview escapes HTML before formatting.
- Validation before save: increasing days, discount range, unknown
  placeholders, and a final attempt as the last step
- Recovery funnel cards: failed invoices, recovered invoices with volume,
  net churn prevented, and recovery rate

Refs Orbit-xyz#190
…eckout

Customers can pay a USDC subscription from another asset in their wallet.

- Pay With selector listing the connected wallet's balances for USDC, EURC,
  XLM and AQUA (AQUA on mainnet only). Assets with no balance are hidden.
- Live swap quote from Horizon strict-receive paths: rate, price impact
  against the mid-market rate (flagged above 5%), and the max input after
  the chosen slippage tolerance (0.5%, 1%, 3%).
- Swap subscribe runs as one Soroban transaction calling
  create_vault_with_swap, so the vault, swap config and payment allowance
  settle together or not at all. The USDC preflight is skipped for swaps.
- Routers come from plan.swap_router_address, which the merchant approves
  on-chain. Without an approved router the swap option is blocked.

Refs Orbit-xyz#191
@vercel

vercel Bot commented Oct 18, 2026

Copy link
Copy Markdown

@Akanimoh12 is attempting to deploy a commit to the Abdqoharr Olayiwola's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@Akanimoh12 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Prudentdev-xyz
Prudentdev-xyz merged commit cd892c6 into Orbit-xyz:main Oct 18, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment