Develop a payment gateway module that integrates with multiple Payment Service Providers (PSPs) via a standardized interface (PSP Adapter), enabling payment processing across arbitrary platforms. The module should support multiple payment methods, configurable through an external configuration file. Payments are processed on behalf of tenants, with tenant-specific PSP configurations stored in the database and linked to tenant IDs. For this scope, PSP Adapters will be implemented for Stripe, PayPal, and a dummy “No-Op Provider,” with the module designed to allow easy addition of new PSPs in the future. One-time payments must be fully supported end-to-end. Recurring payments are out of scope for execution in this freelance task, but the data model and API must be designed to support recurring payments later. All transactions must be fully traceable and auditable.
- Active GitHub account for source code access and collaboration
- Own PayPal developer account for configuring, testing and integrating payment flows
- Own Stripe developer account for configuring, testing and integrating payment flows
- Frameworks:
- Backend: Java 25, Spring Boot (latest), Spring Framework (latest)
- Frontend: Typescript, Vue (latest), Vite (latest), ESLint + Prettier, Vue-Router, Pinia, Vitest, Playwright, Bootstrap, Axios
- Database: PostgreSQL (latest)
- Caching: Redis
- Messaging: RabbitMQ (latest)
- Containerization: Docker / concurrent multi-container operations. Assume deployment in Kubernetes
- Local development uses docker-compose (PostgreSQL + Redis + RabbitMQ).
- Build Tool: Maven (latest)
- API Documentation: OpenAPI 3.0 (YAML)
- Authorization: JWT token verification (OIDC/OAuth2). Tokens are issued by a separate Auth service.
- Structure: Module structure should follow the design of the existing checkout and auditflow modules.
- Configuration: Module static configuration must be managed via Spring Boot, covering:
- Payment methods / common PSP configuration
- Currencies support
- Retry behavior
- etc.
- Security:
- Tokenization: The module must never store raw credit card numbers. It must store PSP tokens/IDs (e.g., Stripe
customer_id,payment_method_id). - Resilience: The
/payendpoint (see below) must support anIdempotency-Keyheader to prevent duplicate charges during network failures. - API Security: All API endpoints must be secured via JWT token (m2m) verification.
- Multi-tenancy:
tenantIdmust be taken from the JWT (tenantIdclaim) and used to scope all tenant data .
- Tokenization: The module must never store raw credit card numbers. It must store PSP tokens/IDs (e.g., Stripe
- Abstraction: Introduce a PSP Abstraction Layer so new PSPs can be added or maintained easily.
- Payment States:
ACTIVE- payment is enabled and will be executed on scheduleINCOMPLETE- payment setup is incomplete; requires user actionPAUSED- payment is temporarily paused but can be resumed laterCLOSED- payment is closed and won't be executed anymore
- Transaction States:
PENDING- payment is initiated but not completed yetSUCCESS- payment completed successfullyFAILED- payment attempt failed; will be retried based on dunning logic
- Money Rules (consistent with checkout):
- All monetary amounts are stored in minor units (e.g., cents for USD/EUR).
- Currency codes must use ISO-4217 (3 uppercase letters).
- Calculated fields are read-only and computed server-side.
- JWT claim names: the tenant identifier claim name is
tenantId. - JWT scopes (space-separated, OAuth2-style):
| Scope | Description |
|---|---|
payment-method:read |
Read available payment methods |
payment-method:admin |
Configure tenant PSP settings |
payment:read |
Read payment details and list payments |
payment:write |
Create payments and execute pay |
transaction:read |
Read transaction details |
- Recommended JWT payload example (user token):
{
"aud": ["checkout", "payment-gateway"],
"sub": "user-123",
"tenantId": "tenant-abs",
"scope": "payment-method:read payment:write payment:read transaction:read",
"roles": [],
"iat": 1707470000,
"exp": 1707473600,
"jti": "2f1c1b6a-2c1e-4f2c-9b6b-9e51cdb1c4a0"
}Notes:
subis the principal id (a user id or a service id depending on the token issuer and flow).scopeis a space-separated string (OAuth2-style).rolesmay be empty (array).
Payment Gateway must NOT create or own a Tenant entity.
- Do not auto-create tenants on first request.
- Do not manage tenant lifecycle in Payment Gateway (create/update/delete/metadata).
tenantId (from JWT) is used only as a scoping key:
- All tenant-scoped endpoints require
tenantId(from JWT). - Database records must include
tenantId. - Tenant-specific PSP configuration must be stored per
tenantId. - Caching / locking keys must include
tenantId.
- YAML (Spring Boot config) contains common/shared payment methods and capabilities.
- Database stores tenant-specific PSP configuration (scoped by
tenantId). - Configuration must be overridable in Kubernetes (e.g., ConfigMap / env overrides).
Example (Milestone 1):
payment-gateway:
payment-methods:
- id: "stripe"
enabled: true
name: "Stripe"
description: "Credit/Debit Card via Stripe"
recurring: true
supported-currencies: ["USD", "EUR", "GBP"]
supported-countries: ["US", "GB", "DE"]
- id: "paypal"
enabled: true
name: "PayPal"
description: "PayPal Checkout"
recurring: false
supported-currencies: ["USD", "EUR"]
supported-countries: ["US", "DE"]
- id: "noop"
enabled: true
name: "No-Op (Test)"
description: "Test payment provider - always succeeds"
recurring: false
supported-currencies: ["USD", "EUR"]
supported-countries: ["US", "DE"]
retry:
max-retries: 3
retry-interval-seconds: 60
backoff-multiplier: 2.0
redis:
idempotency-key-ttl-seconds: 86400 # 24h
distributed-lock-ttl-seconds: 30Conventions:
idmust be unique and stable (internal identifier).supported-currenciesmust use ISO-4217 currency codes (e.g.,USD,EUR).supported-countriesmust use ISO-3166-1 alpha-2 codes (e.g.,US,DE).
- Recurring payment execution (scheduling, renewal engine) is out of scope.
- Data model + API support for recurring payments is in scope.
- One-time and recurring payments share the same core states.
- In the future recurring flow, each successful renewal creates a new Transaction linked to the same Payment.
payment.finalizedwill be published for every successful renewal; only confirmed PSP transactions qualify and PSP webhook status is the source of truth.- Idempotency must be implemented to avoid duplicate events.
- Manual and automatic retries are required.
- Manual retry:
POST /payments/{paymentId}/retryendpoint. - Automatic retries must be configurable per PSP/payment method (see YAML
retrysection above). - Retry configuration:
maxRetries,retryIntervalSeconds,backoffMultiplier.
Redis is used for:
- Idempotency keys: Store processed
Idempotency-Keyvalues pertenantId + paymentIdto prevent duplicate charges. TTL configurable viaredis.idempotency-key-ttl-seconds. - Distributed locks: Coordinate concurrent
/payrequests across multiple pods. TTL configurable viaredis.distributed-lock-ttl-seconds. - Payment method config cache: Cache resolved payment method configurations (YAML + tenant DB config merge) with short TTL to reduce DB lookups.
The PSP Abstraction Layer uses the Strategy Pattern to enable easy addition of new payment providers.
Adding a new PSP should require at best:
- Implement
PaymentProvider. - Annotate with
@Component. - Add YAML configuration entry.
No changes to existing code should be required.
-
API: Implement a RESTful API (via OpenAPI specification) to manage payment gateway methods.
-
Queueing: Final payment transactions should be published to a RabbitMQ queue for further processing by other ecosystem modules.
-
Correlation ID: The
X-Correlation-IDheader must be supported on every request.- The same correlation id must be included in logs, propagated to downstream HTTP calls, and included in RabbitMQ messages (headers and/or payload).
- For webhook callbacks (which arrive without
X-Correlation-ID), the gateway must restore thecorrelationIdfrom the original payment record stored in the database.
-
Events: Publish domain events to RabbitMQ. Payload format must be JSON. Monetary values are integers in minor units (e.g., cents). Consumers must ignore unknown fields (payload evolution is additive).
| Event | Routing Key | Trigger |
|---|---|---|
payment.finalized |
payment.finalized |
Transaction reaches terminal state (SUCCESS or FAILED) |
payment.created |
payment.created |
New payment instance created |
payment.closed |
payment.closed |
Payment closed (manually or after successful one-time payment) |
payment.finalized event example:
{
"event": {
"id": "01J0QW5JZ6R1J7K8YV9E3N2M1P",
"type": "payment.finalized",
"version": 1,
"occurredAt": "2026-02-10T12:34:56Z"
},
"tenantId": "tenant-abs",
"correlationId": "RSVCZ9NYY9GZNMPXI",
"payment": {
"id": "pay_123",
"status": "CLOSED",
"type": "ONE_TIME",
"amount": 1299,
"currency": "USD",
"description": "Order #12345"
},
"transaction": {
"id": "trx_456",
"status": "SUCCESS",
"failureCode": null,
"failureMessage": null
},
"paymentMethod": {
"id": "stripe"
},
"pspReferences": {
"provider": "stripe",
"chargeId": "ch_...",
"paymentIntentId": "pi_..."
}
}- PSP UI Actions: Handle PSP-specific user actions (redirects, pop-ups, inline payment forms, etc.) as part of Payment Gateway UI integration.
- PSP specific frontend dependencies should be exported from the PSP Adapter.
sequenceDiagram
autonumber
participant Client as Client / Checkout UI
participant API as Payment Gateway API
participant CFG as Config (YAML)
participant DB as PostgreSQL
participant PSP as PSP Adapter
participant MQ as RabbitMQ
participant Subscriber as AuditFlow / Checkout / Invoice
Client->>API: GET /payment-methods
API->>CFG: Load payment methods (capabilities: currency, recurring)
API->>DB: Load tenant PSP configs (by tenantId from X-Auth-Tenant, trusted gateway header)
API-->>Client: List of available payment methods
Client->>API: POST /payments (paymentMethodId, purchaseOrderRef, billingInfo, ...)
API->>CFG: Load payment method metadata
API->>DB: Load tenant PSP config for paymentMethodId
API->>DB: Create Payment (INCOMPLETE)
API-->>Client: Payment + optional nextAction (setup)
Client->>API: POST /payments/{payment.id}/pay (Idempotency-Key)
API->>Redis: Check Idempotency-Key (unique per tenant+payment)
API->>DB: Create Transaction (PENDING)
API->>PSP: executePayment() (tokenized data / references)
PSP-->>API: PSP response (status, references, nextAction?)
alt Sync completed (SUCCESS/FAILED)
API->>DB: Update Transaction (SUCCESS/FAILED)
API->>DB: Update Payment (CLOSED for one-time / ACTIVE for recurring)
API->>MQ: Publish payment.finalized (transaction payload, correlationId)
MQ-->>Subscriber: Consume event
API-->>Client: Transaction status
else Requires user action (3DS/redirect)
API->>DB: Update Payment (INCOMPLETE) + store nextAction
API-->>Client: nextAction (redirect / 3DS)
else Async completion (webhook later)
API-->>Client: 202 Accepted (Transaction PENDING)
PSP-->>API: Webhook/notification (later)
API->>DB: Update Transaction (SUCCESS/FAILED)
API->>MQ: Publish payment.finalized (transaction payload, correlationId)
MQ-->>Subscriber: Consume event
end
This section outlines the proposed RESTful API endpoints for the Payment Gateway module.
Every endpoint supports correlation tracing via the X-Correlation-ID header. All endpoints return errors using the standard ErrorResponse schema (see below).
Base path: /api/v1
Consistent with checkout and auditflow modules:
{
"code": "NOT_FOUND",
"message": "Payment with ID '550e8400-...' was not found.",
"timestamp": "2026-02-10T12:34:56Z",
"traceId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}Error codes:
| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND |
404 | Requested resource not found |
VALIDATION_ERROR |
400 | Request payload failed validation |
CONFLICT |
409 | Duplicate idempotency key or state conflict |
INTERNAL_ERROR |
500 | Unexpected server error |
PSP_ERROR |
502 | Upstream PSP communication failure |
MISSING_TENANT_ID |
400 | Missing tenantId in JWT |
UNAUTHORIZED |
401 | Invalid or missing JWT |
FORBIDDEN |
403 | Insufficient scopes |
IDEMPOTENCY_CONFLICT |
409 | Idempotency-Key reused with different request body |
PAYMENT_NOT_PAYABLE |
409 | Payment is in a state that does not allow /pay (e.g., already CLOSED) |
GET /payment-methods
Returns the list of available payment methods for the tenant, filtered by YAML config and tenant PSP configuration.
-
Scopes required:
payment-method:read -
Query Parameters:
currency(string) (optional): Filter by supported currency code (ISO-4217).country(string) (optional): Filter by supported country code (ISO-3166-1 alpha-2).
-
Response
200 OK: -
Response:
-
id(string): Unique identifier for the payment method. -
name(string): Name of the payment method. -
description(string): Description of the payment method. -
icon(base64) (optional): Data for the PSP icon image. -
recurring(boolean): Indicates if the method supports recurring payments.
PUT /payment-methods/{paymentMethodId}/config
Configure or update PSP-specific settings for the tenant. The tenant is determined from the JWT token.
-
Scopes required:
payment-method:admin -
Request Body:
-
pspConfig(object): PSP-specific configuration parameters. -
Response:
204 No Contenton success.400ifpaymentMethodIdis not a valid/known payment method.
GET /payment-methods/{paymentMethodId}/config
Retrieve the current PSP configuration for the tenant. Sensitive fields (API keys) must be masked in the response.
-
Scopes required:
payment-method:admin -
Response
200 OK:
{
"paymentMethodId": "stripe",
"tenantId": "tenant-abs",
"enabled": true,
"config": {
"apiKey": "sk_test_...****",
"webhookSecret": "whsec_...****",
"merchantId": "acct_..."
},
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-02-01T14:30:00Z"
}POST /payments
Initiate a new payment instance (one-time or recurring) and capture payment details. No actual charge is performed yet. The tenant is determined from the JWT token.
-
Scopes required:
payment:write -
Request Body:
-
paymentMethodId(string): id of the selected payment method. -
purchaseOrder(object): Details from the checkout module.- ... including
recurring(boolean): Indicates if the payment is recurring.
-
billingInfo(object): Billing information. -
shippingInfo(object) (optional): Shipping information. -
extra(object) (optional): Additional metadata. -
Response:
-
payment(object): The created Payment object. -
nextAction(object): returns optional next action required to complete the payment details capture via specific PSP flow.type(string): e.g., "none", "redirect", "3ds-challenge".details(object): Metadata required for the next action.
POST /payments/{payment.id}/pay
Execute a payment via the external PSP using stored captured payment details and create a payment transaction. For non-recurring payments, this operation closes the payment on success.
- Scopes required:
payment:write - Headers:
Idempotency-Key(string, required): Unique key pertenantId + paymentIdcombination.
NOTE: PSP may require delayed completion with later webhook notification. The architecture must handle this by returning 202 Accepted and updating the transaction status asynchronously upon webhook receipt.
- Response
200 OK(synchronous completion):
NOTE: PSP may imply delayed completion of the payment with later notification. Ensure architecture is prepared for handling this scenario by capturing the PSP notification and adjusting the transaction status in the background.
Transaction status transitions to SUCCESS or FAILED are to be published in the queueing service.
- Response:
transaction(object): Payment transaction details.id(string): Unique identifier for the transaction.status(string): Transaction status.pspData(object): Raw data returned from the PSP.
GET /payments/{payment.id}
Retrieve the status and details of an existing payment. Can be used to restore (possibly incomplete) nextAction processing
- Response:
payment(object): The created Payment object.nextAction(object):type(string): e.g., "none", "redirect", "3ds-challenge".details(object): Metadata required for the next action.
POST /payments/{payment.id}/close
Close an existing payment. No further pay operations can be executed.
GET /transactions/{transaction.id}
Retrieve the status and details of a payment transaction.
- Response:
transaction(object): Payment transaction details.id(string): Unique identifier for the transaction.status(string): Transaction status.pspData(object): Raw data returned from the PSP.
- OpenAPI Spec: Fully specified YAML file for the RESTful API, including all endpoints, schemas, error model, and examples.
- Java Module: Implementing the functionality described above; including PayPal, Stripe and NoOp PSP adapters.
- Unit Tests: Covering all major functionalities.
- Configuration:
- Structured
application.yamlfor integrated payment methods. - PSP-specific configuration instructions/templates/examples for a tenant.
- Structured
- Database: Flyway migration scripts in
resources/db/migration. - Dockerfile: For containerizing the module.
- Create docker-compose for local development (single instance) including PostgreSQL, Redis, and RabbitMQ.
- Documentation: Setup, configuration, usage guide, contributor guide.
- CI/CD:
.githubworkflows pipeline to build, test.
- Total workload will be split into milestones defined as below.
- At the beginning of each milestone, a new PR must be created targeting the main branch of the Payment Gateway repo.
- Daily work progress must be frequently committed (one daily commit at minimum).
- Early & timely communication via GitHub Issues and PRs only. Expect on-the-fly comments to the commits.
- Each milestone must be approved and merged before proceeding to the next one.
- Scope review and adjustments may be done at the end of each milestone if necessary. Additional work beyond the defined milestones will be treated as out-of-scope and must be agreed separately.
- Approved and merged milestones PRs are considered as completed and entitled for payment.
- Failure to complete a milestone within the agreed timeframe may result in project termination.
- Milestone 1: Project Setup & Basic Architecture
- ✅ (done) - Setup Spring Boot project with necessary dependencies.
- ✅ (done) - Dockerfile and basic CI/CD pipeline.
- Design OpenAPI specification with generated Java interface and create API stubs with log output.
- Milestone 2: PSP Abstraction Layer & NoOp Implementation
- Implement PSP Abstraction Layer using Strategy Pattern.
- Create NoOp PSP adapter for testing.
- Create needed database entities and Flyway migration scripts.
- Implement all endpoints with basic logic.
- Milestone 3: Stripe Integration
- Implement Stripe PSP adapter.
- Complete payment flow for Stripe.
- Unit tests for Stripe integration.
- Milestone 4: PayPal Integration
- Implement PayPal PSP adapter.
- Complete payment flow for PayPal.
- Unit tests for PayPal integration.
Release Condition: The Pull Request to the Payment Gateway repo must be accepted and merged into the main branch to release the freelance payment feature.
- Code follows standard Java best practices (consistent with checkout / auditflow modules) and is well-documented.
- PSP Abstraction Layer is implemented using the Strategy Pattern (e.g.,
StripeProvider,PayPalProvider, andNoOpProviderimplement a commonPaymentProviderinterface). - Integrated PSPs:
- Stripe (latest API) - https://docs.stripe.com/api
- PayPal (latest API) - https://developer.paypal.com/api/rest/
- None (NoOp for testing)
- One-time payments can be demoed with all integrated PSPs using sandbox accounts (personal developer accounts). Recurring payment execution is out of scope for this task.
- Async Messaging: RabbitMQ producer is implemented for
payment.finalized,payment.created, andpayment.closedevents with transaction payload. - Webhook handling: PSP webhooks are received via
POST /webhooks/{provider}, verified using PSP-specific signatures, and used to settle async transactions. - Distributed Environments / Kubernetes safety:
- All functionality can run reliably on Kubernetes
- Execution is idempotent to prevent duplicate processing
- Components are safe to run in parallel across multiple pods
- Coordination and state handling support distributed execution without race conditions
- Traceability: Every log entry and RabbitMQ message must contain a
correlationIdtraceable across ecosystem modules. - Docker container builds successfully and runs without errors.
- All unit tests pass with >80% code coverage.
- API endpoints function exactly as specified in the OpenAPI documentation.
- Error model follows the standard
ErrorResponseschema consistent with checkout and auditflow modules. - Logging Policy: no sensitive information in the logs, such as PAN/PII, credentials, etc.
- Payment methods are configurable via YAML and retrievable via API.
- Vulnerability Scanning: no
HighandCriticalvulnerabilities (static code analysis, dependencies, docker, etc.) with GitHub Code scanning / CodeQL, Trivy for Docker