Skip to content

feat(ledger): add GNU Taler payment gateway driver#4

Open
roncodes wants to merge 1 commit intomainfrom
feature/taler-payment-gateway
Open

feat(ledger): add GNU Taler payment gateway driver#4
roncodes wants to merge 1 commit intomainfrom
feature/taler-payment-gateway

Conversation

@roncodes
Copy link
Copy Markdown
Member

Summary

Adds GNU Taler as a first-class payment gateway option in Fleetbase Ledger, following the existing driver-based architecture.

GNU Taler is a privacy-preserving electronic payment system that provides customer anonymity while ensuring merchants remain fully accountable and taxable. It is free software (GNU project) and is already used by several European institutions and banks.


Files Changed

server/src/Gateways/TalerDriver.php (new)

A complete implementation of AbstractGatewayDriver for GNU Taler:

  • getCode()'taler'
  • getName()'GNU Taler'
  • getCapabilities()['purchase', 'refund', 'webhooks']
  • getConfigSchema() → dynamic form fields (backend_url, instance_id, api_token) rendered automatically by the Fleetbase gateway settings UI — no frontend changes required
  • purchase() — POSTs to /instances/{id}/private/orders, embeds invoice_uuid in the Taler contract terms for webhook traceability, fetches taler_pay_uri from the order status endpoint, returns GatewayResponse::pending() with the URI for wallet redirect
  • handleWebhook() — receives order_id from Taler, re-queries the private API to verify payment status (prevents spoofing/replay attacks), extracts invoice_uuid from contract terms, returns GatewayResponse::success() which triggers the existing HandleSuccessfulPayment listener
  • refund() — POSTs to /private/orders/{order_id}/refund, returns GatewayResponse::success() with EVENT_REFUND_PROCESSED
  • toTalerAmount() / fromTalerAmount() — bidirectional conversion between Fleetbase integer cents and Taler CURRENCY:UNITS.FRACTION strings, respecting the Fleetbase monetary storage standard

server/src/PaymentGatewayManager.php (modified)

  • Added TalerDriver import
  • Registered 'taler' in getRegisteredDriverCodes()
  • Added createTalerDriver() factory method

server/tests/Gateways/TalerDriverTest.php (new)

16 Pest tests using Http::fake() — no real backend required:

Group Tests
Driver metadata code, name, capabilities, config schema
purchase() happy path, correct amount format, invoice_uuid embedding, backend error, missing order_id
handleWebhook() verified paid order, missing order_id, unpaid order, backend error
refund() happy path, correct amount format, backend error
Amount conversion zero amount, single-digit fraction

How the Payment Flow Works

Customer → Fleetbase UI → purchase() → Taler Backend (POST /orders)
                                     ← order_id + taler_pay_uri
         ← redirect to taler_pay_uri
Customer → Taler Wallet → pays order
Taler Backend → POST /ledger/webhooks/taler  { order_id }
             → handleWebhook() → GET /orders/{order_id} (verify)
             → GatewayResponse::success() → HandleSuccessfulPayment
             → Invoice marked paid, journal entries created

Integration Notes

  • The webhook route POST /ledger/webhooks/taler is served automatically by the existing WebhookControllerno route changes needed
  • All monetary values follow the Fleetbase standard: integers in the smallest currency unit (cents), converted to/from Taler string format
  • The API token is stored encrypted via the existing Gateway model encrypted:array cast
  • Sandbox/test mode is handled by pointing backend_url at a Taler test instance (e.g. https://backend.demo.taler.net/)
  • No new Composer dependencies are introduced — uses Laravel's built-in Http facade

Refs: https://docs.taler.net/core/api-merchant.html

Implements TalerDriver as a first-class payment gateway option in the
Fleetbase Ledger module, following the existing driver-based architecture.

## What was added

### server/src/Gateways/TalerDriver.php
- Extends AbstractGatewayDriver and implements GatewayDriverInterface
- getCode(): 'taler'
- getName(): 'GNU Taler'
- getCapabilities(): ['purchase', 'refund', 'webhooks']
- getConfigSchema(): dynamic form fields for backend_url, instance_id,
  api_token — rendered automatically by the Fleetbase gateway UI
- purchase(): POSTs to Taler Merchant Backend /private/orders, embeds
  invoice_uuid in contract terms, returns GatewayResponse::pending()
  with taler_pay_uri for wallet redirect
- handleWebhook(): receives order_id, re-queries private API to verify
  payment status (prevents spoofing), returns GatewayResponse::success()
  which triggers the existing HandleSuccessfulPayment listener
- refund(): POSTs to /private/orders/{order_id}/refund, returns
  GatewayResponse::success() with EVENT_REFUND_PROCESSED
- toTalerAmount() / fromTalerAmount(): bidirectional conversion between
  Fleetbase integer cents and Taler 'CURRENCY:UNITS.FRACTION' strings

### server/src/PaymentGatewayManager.php
- Added TalerDriver import
- Registered 'taler' in getRegisteredDriverCodes()
- Added createTalerDriver() factory method

### server/tests/Gateways/TalerDriverTest.php
- Full Pest test suite covering:
  - Driver metadata (code, name, capabilities, config schema)
  - purchase() happy path and failure paths
  - handleWebhook() happy path, unpaid order, missing order_id
  - refund() happy path and failure paths
  - Amount conversion edge cases (zero, single-digit fraction)

## Integration notes
- Webhook route is automatically served at POST /ledger/webhooks/taler
  by the existing WebhookController — no route changes needed
- All monetary values follow the Fleetbase standard: integers in the
  smallest currency unit (cents), converted to/from Taler string format
- API token is stored encrypted via the existing Gateway model cast
- Sandbox mode is handled by pointing backend_url at a test instance

Refs: https://docs.taler.net/core/api-merchant.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant