[Tech Spec] PlaceToPay — VoidPostCapture
- Dominant language
- Python
- Stars
- 4
- Forks
- 6
- PR merge metrics
- No merged PRs in 30d
Description
# PlaceToPay — VoidPostCapture
**Complexity:** low
**Generated by:** Grace pipeline run `run-2026-05-21T23-32-25-523Z-5aba32`
## Summary
Implement VoidPostCapture payment method for PlaceToPay connector. Technical Specification — PlaceToPay
## Scope
# Technical Specification — PlaceToPay
## 1. Connector Profile
| Property | Value |
|---|---|
| Connector Name | PlaceToPay |
| Primary Flow Scope | Card Payments (Authorize, Capture, Void, VoidPostCapture, Refund, PSync, RSync) |
| API Family | REST/JSON |
| Production Host | `https://checkout.placetopay.com/rest/gateway` |
| Sandbox Host | `https://test.placetopay.com/rest/gateway` |
| Regional Variants | None documented; single international endpoint |
---
## 2. Authentication
**Scheme:** Custom HMAC-SHA256 digest (WS-Security style)
Every request body carries an `auth` object:
```json
{
"auth": {
"login": "",
"tranKey": "",
"nonce": "",
"seed": ""
}
}
```
**Credentials required:**
- `login` — Merchant login identifier (plain string, sent as-is)
- `tran_key` / secret — Merchant secret key used as the HMAC input (never sent directly)
**Construction algorithm:**
1. Generate 16 cryptographically random bytes → `nonce_bytes`
2. Base64-encode `nonce_bytes` → `nonce`
3. Format current UTC time as `YYYY-MM-DDTHH:MM:SS+00:00` → `seed`
4. Compute `SHA256(nonce_bytes || seed_bytes || secret_bytes)` (concatenation of raw bytes)
5. Base64-encode the SHA256 digest → `tranKey`
**Implementation notes:**
- `nonce` and `seed` are request-unique; replay is prevented by the timestamp.
- The `+00:00` suffix must be explicit (not `Z`).
- There are no HTTP Authorization headers; auth lives entirely in the JSON body.
---
## 3. Supported Flows
| Flow | HTTP Method | Path | Notes |
|---|---|---|---|
| Authorize | POST | `/process` | Card-present or card-not-present authorization |
| PSync | POST | `/query` | Retrieve payment status by `internalReference` |
| Capture | POST | `/transaction` | `action=CHECKOUT` — confirm/settle a pre-authorized payment |
| Void (pre-auth) | POST | `/transaction` | `action=VOID` — ca
## Out of Scope
Not specified in techspec
## Technical Constraints
- Follow existing connector patterns in the codebase
---
## Full Tech Spec
# Technical Specification — PlaceToPay
## 1. Connector Profile
| Property | Value |
|---|---|
| Connector Name | PlaceToPay |
| Primary Flow Scope | Card Payments (Authorize, Capture, Void, VoidPostCapture, Refund, PSync, RSync) |
| API Family | REST/JSON |
| Production Host | `https://checkout.placetopay.com/rest/gateway` |
| Sandbox Host | `https://test.placetopay.com/rest/gateway` |
| Regional Variants | None documented; single international endpoint |
---
## 2. Authentication
**Scheme:** Custom HMAC-SHA256 digest (WS-Security style)
Every request body carries an `auth` object:
```json
{
"auth": {
"login": "",
"tranKey": "",
"nonce": "",
"seed": ""
}
}
```
**Credentials required:**
- `login` — Merchant login identifier (plain string, sent as-is)
- `tran_key` / secret — Merchant secret key used as the HMAC input (never sent directly)
**Construction algorithm:**
1. Generate 16 cryptographically random bytes → `nonce_bytes`
2. Base64-encode `nonce_bytes` → `nonce`
3. Format current UTC time as `YYYY-MM-DDTHH:MM:SS+00:00` → `seed`
4. Compute `SHA256(nonce_bytes || seed_bytes || secret_bytes)` (concatenation of raw bytes)
5. Base64-encode the SHA256 digest → `tranKey`
**Implementation notes:**
- `nonce` and `seed` are request-unique; replay is prevented by the timestamp.
- The `+00:00` suffix must be explicit (not `Z`).
- There are no HTTP Authorization headers; auth lives entirely in the JSON body.
---
## 3. Supported Flows
| Flow | HTTP Method | Path | Notes |
|---|---|---|---|
| Authorize | POST | `/process` | Card-present or card-not-present authorization |
| PSync | POST | `/query` | Retrieve payment status by `internalReference` |
| Capture | POST | `/transaction` | `action=CHECKOUT` — confirm/settle a pre-authorized payment |
| Void (pre-auth) | POST | `/transaction` | `action=VOID` — cancel before capture |
| VoidPostCapture | POST | `/transaction` | `action=REVERSE` with `authorization` — reverse a captured/settled transaction |
| Refund | POST | `/transaction` | `action=REVERSE` with `authorization` — full reversal only (partial not supported) |
| RSync | POST | `/query` | Retrieve refund/reversal status by `internalReference` |
| Webhooks | N/A | N/A | Not implemented / not documented in current integration |
> **VoidPostCapture vs Void distinction:** PlaceToPay uses `action=VOID` to cancel a transaction that has been authorized but not yet captured. Once a transaction is captured (`action=CHECKOUT`), the only way to reverse it is `action=REVERSE`. The `authorization` code returned in the capture response must be included in the Reverse request.
---
## 4. Request Schema Highlights
### Authorize — POST `/process`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"payment": {
"reference": "",
"description": "",
"amount": {
"currency": "USD",
"total": 1000
}
},
"instrument": {
"card": {
"number": "4111111111111111",
"expiration": "12/26",
"cvv": "123"
}
},
"ipAddress": "",
"userAgent": ""
}
```
- All fields are **required** for card payments.
- `amount.total` is in **minor units** (cents).
- `expiration` format: `MM/YY` (slash-delimited, 2-digit year).
- `ipAddress` and `userAgent` come from browser_info in the authorize request.
### Capture — POST `/transaction`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"internalReference": 123456789,
"action": "CHECKOUT"
}
```
### Void (pre-auth) — POST `/transaction`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"internalReference": 123456789,
"action": "VOID"
}
```
### VoidPostCapture — POST `/transaction`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"internalReference": 123456789,
"action": "REVERSE",
"authorization": ""
}
```
- `internalReference`: PlaceToPay's numeric transaction ID returned in the Authorize/Capture response (stored as `connector_transaction_id`). Must be parsed as `u64`.
- `authorization`: The authorization code returned in the Capture response (`connector_metadata`). Required for post-capture reversal.
- `action` values (enum, SCREAMING_SNAKE_CASE): `REFUND`, `REVERSE`, `VOID`, `PROCESS`, `CHECKOUT`
### PSync / RSync — POST `/query`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"internalReference": 123456789
}
```
### Refund — POST `/transaction`
```json
{
"auth": { "login": "…", "tranKey": "…", "nonce": "…", "seed": "…" },
"internalReference": 123456789,
"action": "REVERSE",
"authorization": ""
}
```
- Only **full refunds** are supported. Partial refund amount ≠ payment amount → `NotSupported` error.
---
## 5. Response Schema Highlights
### Payment Response (Authorize / Capture / Void / VoidPostCapture)
```json
{
"status": {
"status": "APPROVED"
},
"internalReference": 123456789,
"authorization": "AUTH_CODE_STRING"
}
```
| Field | Type | Notes |
|---|---|---|
| `status.status` | enum (string) | See Transaction Status Values below |
| `internalReference` | u64 | PlaceToPay's numeric transaction ID; used as `connector_transaction_id` |
| `authorization` | string (optional) | Authorization code; stored as `connector_metadata`; required for VoidPostCapture/Refund |
**Transaction Status Values:**
| `status` Value | Maps To AttemptStatus |
|---|---|
| `APPROVED` | `Charged` |
| `OK` | `Charged` |
| `FAILED` | `Failure` |
| `REJECTED` | `Failure` |
| `ERROR` | `Failure` |
| `PENDING` | `Pending` |
| `PENDING_VALIDATION` | `Pending` |
| `PENDING_PROCESS` | `Pending` |
### Refund Response (Refund / RSync)
```json
{
"status": {
"status": "REFUNDED"
},
"internalReference": 123456789
}
```
**Refund Status Values:**
| `status` Value | Maps To RefundStatus |
|---|---|
| `OK` | `Success` |
| `APPROVED` | `Success` |
| `REFUNDED` | `Success` |
| `FAILED` | `Failure` |
| `REJECTED` | `Failure` |
| `ERROR` | `Failure` |
| `PENDING` | `Pending` |
| `PENDING_PROCESS` | `Pending` |
| `PENDING_VALIDATION` | `Pending` |
---
## 6. Error Handling
### Error Response Schema
```json
{
"status": {
"status": "FAILED",
"message": "",
"reason": ""
}
}
```
| HTTP Status | `status.status` | Cause |
|---|---|---|
| 400 | `FAILED` | Malformed request, missing required fields |
| 401 | `FAILED` | Invalid or expired authentication (bad login/tranKey/nonce) |
| 404 | `FAILED` | `internalReference` not found |
| 422 | `FAILED` | Business rule violation (e.g., attempting REVERSE on non-captured transaction) |
| 500 | `FAILED` | Internal PlaceToPay server error |
- `status.reason` → mapped to `ErrorResponse.code` (falls back to `NO_ERROR_CODE` if absent)
- `status.message` → mapped to `ErrorResponse.message` and `ErrorResponse.reason` (falls back to `NO_ERROR_MESSAGE` if absent)
- `connector_transaction_id` is not populated in error responses
---
## 7. Webhooks / Async Notifications
**Status:** Not implemented in the current PlaceToPay connector integration.
- No webhook subscription endpoint is configured.
- No webhook signature verification logic exists.
- Async state changes must be polled via PSync (POST `/query`).
- The `IncomingWebhook` trait is implemented as a no-op stub.
**Documented gaps:**
- PlaceToPay's API may support push notifications (webhooks) for production environments, but the integration relies solely on polling for payment status updates.
- No retry policy, delivery format, or verification mechanism is documented or implemented.
---
## 8. References
- Connector implementation: `crates/integrations/connector-integration/src/connectors/placetopay.rs`
- Transformer types: `crates/integrations/connector-integration/src/connectors/placetopay/transformers.rs`
- Sandbox base URL: `https://test.placetopay.com/rest/gateway` (from `config/development.toml`)
- Production base URL: `https://checkout.placetopay.com/rest/gateway` (from `config/production.toml`)
- Field probe data: `data/field_probe/placetopay.json`
- PlaceToPay official documentation: `https://docs.placetopay.com/` (not scraped; WebFetch not invoked per Phase 1c skip instruction)
- Integration test spec: `crates/internal/integration-tests/src/connector_specs/placetopay/`
---
## 9. VoidPostCapture Implementation Notes
This section is specific to the `VoidPostCapture` flow being added.
### Semantic distinction from pre-auth Void
| Scenario | PlaceToPay Action | When Used |
|---|---|---|
| Cancel before capture | `VOID` | Payment authorized but not yet settled |
| Reverse after capture | `REVERSE` | Payment captured/settled; full reversal needed |
### Request construction for VoidPostCapture
1. Retrieve `connector_transaction_id` from the router data — this is `internalReference` (parse as `u64`).
2. Retrieve `authorization` from `connector_metadata` stored at capture time.
3. Send POST to `/transaction` with `action=REVERSE`, `internalReference`, and `authorization`.
### Response handling
- Same `PlacetopayPaymentsResponse` struct as Capture/Void — reuse existing response type.
- Status mapping follows the same `PlacetopayTransactionStatus → AttemptStatus` conversion.
- A successful VoidPostCapture returns `APPROVED` or `OK` status.
### Constraint
- `authorization` code must be available from the preceding Capture response. If not present, the REVERSE request may be rejected by PlaceToPay.
---
## 10. API Call Sequences
### Standard Authorize → Capture → VoidPostCapture
```
POST /process → { internalReference: 123, authorization: "AUTH" } [Authorize]
POST /transaction → action=CHECKOUT, internalReference=123 [Capture]
POST /transaction → action=REVERSE, internalReference=123, authorization="AUTH" [VoidPostCapture]
```
### Authorize → Void (pre-auth)
```
POST /process → { internalReference: 123 } [Authorize]
POST /transaction → action=VOID, internalReference=123 [Void]
```
---
## 11. Field Dependency Analysis
| Field | Source | Required For | Notes |
|---|---|---|---|
| `auth.login` | ConnectorConfig.login | All flows | Merchant identifier |
| `auth.tranKey` | Derived (SHA256 of nonce+seed+secret) | All flows | Recomputed per request |
| `auth.nonce` | Random 16 bytes, base64 | All flows | Per-request uniqueness |
| `auth.seed` | Current UTC timestamp | All flows | Tied to nonce for replay protection |
| `internalReference` | Authorize response | Capture, Void, VoidPostCapture, PSync, Refund, RSync | Stored as connector_transaction_id |
| `authorization` | Capture/Authorize response (connector_metadata) | VoidPostCapture, Refund | Required for post-capture operations |
| `action` | Flow type | Capture, Void, VoidPostCapture, Refund | Enum: CHECKOUT/VOID/REVERSE |
| `payment.reference` | connector_request_reference_id | Authorize | Merchant-side order reference |
| `payment.amount.total` | minor_amount | Authorize | Minor unit currency amount |
| `instrument.card.*` | PaymentMethodData::Card | Authorize | Card details |
| `ipAddress` | browser_info.ip_address | Authorize | Required for card authorization |
| `userAgent` | browser_info.user_agent | Authorize | Required for card authorization |
---
## 12. UNDECIDED Fields
| Field | Location | Question |
|---|---|---|
| `authorization` in VoidPostCapture | Request body | Confirm whether PlaceToPay requires `authorization` for REVERSE on all captured transactions, or only for specific card networks |
| Partial reversal | VoidPostCapture | Confirm whether PlaceToPay supports partial REVERSE amounts (currently assumed: no, same as Refund) |
| `action=REFUND` | PlacetopayNextAction enum | Distinguish from REVERSE — may be for partial amounts or different reversal semantics |
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in crates/integrations/connector-integration/src/connectors/placetopay.rs and its transformers.rs, comparing the existing Capture and Void flows. Then inspect crates/internal/integration-tests/src/connector_specs/placetopay/. Done means VoidPostCapture sends POST /transaction with action=REVERSE, internalReference, and capture authorization, and reuses the documented response status mapping.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- payments
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100