Skip to content

Add pre-authorization (confirmation_mode) support to Merchant resources - #165

Open
paulosouza-stark wants to merge 1 commit into
masterfrom
feature/pre-authorization
Open

paulosouza-stark wants to merge 1 commit into
masterfrom
feature/pre-authorization

Conversation

@paulosouza-stark

Copy link
Copy Markdown
Contributor

Description and impact

The feat/pre-approve branch of api-v2-ms-card-merchant introduced manual confirmation mode (pre-authorization) for merchant purchases: a new confirmationMode field (automatic / manual, default automatic) on MerchantSession and MerchantPurchase. In manual mode the purchase is approved but only captured after an explicit confirmation (PATCH /merchant-purchase/{id} with status=confirmed), and a new DELETE /merchant-purchase/{id} endpoint cancels (approved) or fully reverses (confirmed) a purchase.

The Python SDK did not expose confirmationMode nor the delete endpoint, so integrators could not use pre-authorization through the SDK. This PR adds that support.

Planning card: ACQ-413.

Change

  • Added the confirmation_mode attribute to the MerchantSession, MerchantPurchase and MerchantSession.Purchase resources. It is serialized as confirmationMode and omitted when None (so the server default automatic applies). starkcore.from_api_json silently drops unknown fields, so the attribute must exist in the resource for the value to be exposed on responses.
  • Added merchantpurchase.delete(id)DELETE /merchant-purchase/{id} (cancels an approved purchase or fully reverses a confirmed one; the operation is inferred server-side from the current status).
  • Confirming a pre-authorized purchase uses the existing merchantpurchase.update(id, status="confirmed", amount=...).
  • Updated README.md (field in the session/purchase examples + new Confirm/Cancel a MerchantPurchase sections), CHANGELOG.md ([Unreleased]), and the builders/tests under tests/.
  • The POST /merchant-purchase/{id} route listed in the service permissions.yaml was intentionally not implemented (it has no handler). The manual + debit restriction is enforced server-side; the SDK just forwards the value.

SDK usage examples

1. Create a pre-authorization session (confirmation_mode="manual")

import starkbank

session = starkbank.merchantsession.create(
    starkbank.MerchantSession(
        allowed_funding_types=["credit"],
        allowed_installments=[
            starkbank.merchantsession.AllowedInstallment(total_amount=0, count=1),
        ],
        expiration=3600,
        challenge_mode="disabled",
        confirmation_mode="manual",
        tags=["pre-auth"],
    )
)

# session.id                -> "5950134772826112"
# session.uuid              -> "0bb894a2697d41d99fe02cad2c00c9bc"
# session.confirmation_mode -> "manual"
# session.status            -> "created"

2. Create a purchase directly in manual mode (optional — credit only)

purchase = starkbank.merchantpurchase.create(
    starkbank.MerchantPurchase(
        amount=10000,
        installment_count=1,
        card_id="6295415968235520",
        funding_type="credit",
        confirmation_mode="manual",
    )
)

# purchase.status            -> "approved"   (approved, not yet captured)
# purchase.confirmation_mode -> "manual"

3. Confirm (capture) the pre-authorized purchase

purchase = starkbank.merchantpurchase.update(
    id="5189831499972623",
    status="confirmed",
    amount=10000,
)

# purchase.status            -> "confirmed"
# purchase.amount            -> 10000
# purchase.confirmation_mode -> "manual"

4. Cancel an approved purchase / fully reverse a confirmed one

purchase = starkbank.merchantpurchase.delete("5189831499972623")

# purchase.status -> "canceling"   (approved -> canceling)
#                 -> "reversing"   (confirmed -> reversing)

print(...) on these objects renders as MerchantSession[<id>] / MerchantPurchase[<id>]; the values shown above are the attributes populated on the returned object.

Rollback Plan

  • Additive, backwards-compatible change to a client SDK (new optional attribute + new method); no API/database impact.
  • If needed, revert commit e82170e (Added pre-authorization) and/or pin the dependency to the previously released SDK version.

Acceptance Criteria

  • merchantsession.create(...) with confirmation_mode="manual" returns a session and confirmation_mode round-trips on the response.
  • A purchase created in a manual session is approved (not captured) until confirmed.
  • merchantpurchase.update(id, status="confirmed", amount=...) confirms (captures) a manual purchase.
  • merchantpurchase.delete(id) cancels an approved purchase and fully reverses a confirmed one.
  • Existing MerchantSession/MerchantPurchase tests keep passing.

@edu-stark edu-stark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review against the Python reference and the backend services (read-only pass over every open PR, 2026-09-17). Findings below; happy to help with the rebase once the content points are addressed.

#165 — Add pre-authorization (confirmation_mode) support to Merchant resource

Author: Paulo · 1 commit (2457377 Added pre-authorization) · 10 files · base 9ef6834 (2026-06-29) · approved, CONFLICTING

Verdict: request-changes

The behaviour is right and matches the backend. The problem is documentation shape: the branch
forked one commit before master's docstring pass, and every code file auto-merges cleanly, so the
missing docstring lines will land silently unless they are added before merge.

Rebase status

  • Divergence: 8 behind, 1 ahead (the PR facts said 3 behind — stale).
  • Merge base: 9ef6834 = "Merge pull request #167 from starkbank/bump".
  • No merge commits on the branch; single clean commit. Fork-point rule satisfied once rebased.
  • Conflicting files: CHANGELOG.md only. README.md,
    starkbank/merchantpurchase/__merchantpurchase.py, starkbank/merchantsession/__merchantsession.py
    and starkbank/merchantsession/__purchase.py all auto-merge (verified with
    merge-tree --write-tree origin/pr-165 origin/master → tree 178060b).
  • Rebase is mechanical. The CHANGELOG conflict is two additive bullet lists under the same
    ## [Unreleased] / ### Added heading; resolution is to concatenate the PR's two bullets with
    master's VerifiedAccount/VerifiedTransfer bullets and keep master's ### Fixed / - Docstrings.
  • Merged README section order is sane: ## Confirm a MerchantPurchase and
    ## Cancel a MerchantPurchase land between ## Get a MerchantPurchase and
    ## Query MerchantInstallments.

Backend verification (api-v2-ms-card-merchant)

Everything the PR asserts checks out:

  • Enum is exactly automatic / manualutils/merchantSession.py:23-25
    (class MerchantSessionConfirmationMode).
  • Default is automatic, applied server-side — models/merchantPurchase.py:85
    ("confirmationMode": self.confirmationMode or MerchantSessionConfirmationMode.automatic) and
    utils/merchantSession.py:41.
  • Which resources carry it:
    • MerchantSession — writable input: middlewares/merchantSession.py:196 lists
      confirmationMode in optionalParameters; returned via utils/merchantSession.py:41.
    • MerchantPurchase — writable input: middlewares/merchantPurchase.py:335
      optionalParameters = ["challengeMode", "confirmationMode", ...]; returned via
      models/merchantPurchase.py:85.
    • MerchantSession.Purchasereturn-only. middlewares/merchantSessionPurchase.py:36
      accepts only ["installmentCount", "tags"] plus the challenge fields; the value is inherited
      from the session (tests/handlers/public/merchantSessionPurchaseTest.py:273,288).
  • "credit only" is real: middlewares/merchantSession.py:523-527 rejects debit + manual, and the
    purchase middleware does the same (tests/handlers/public/merchantPurchaseTest.py:279,
    LocalApiMessageKey.debitNotAllowedInManualConfirmationMode).
  • Confirm-via-PATCH is real: models/merchantPurchase.py:29-30
    patchStatus() == ["confirmed", "reversed", "canceled"];
    handlers/public/merchantPurchaseInfo.py:45-66 routes confirmed to _handlePurchaseConfirmation;
    middlewares/merchantPurchase.py:223-250 requires confirmationMode == manual and
    status == approved. PATCH requires both status and amount
    (middlewares/merchantPurchase.py:88) — the README example correctly passes amount.
  • DELETE is real: routes/public.py:41handlers/public/merchantPurchaseInfo.py:72, with
    _checkPurchaseDelete (middlewares/merchantPurchase.py:152-168) inferring the operation from
    current status: approved→canceled, confirmed→reversed, paid→reversed.
  • rest.delete_id sends no payload (core-python/starkcore/utils/rest.py:178-191) and the handler's
    getJsonBody() returns {} on an empty body, so the bodyless DELETE is accepted.

Blocking findings

  1. starkbank/merchantpurchase/__merchantpurchase.py:16confirmation_mode added to the
    signature but absent from the class docstring. Master landed a full docstring pass on this class
    (### Fixed / - Docstrings in Unreleased) with explicit ## Parameters (optional) and
    ## Attributes (return-only) lists. Verified on the merged tree 178060b: the docstring ends at
    - updated [...] and never mentions confirmation_mode. Must be added under
    ## Parameters (optional).

  2. starkbank/merchantsession/__merchantsession.py:17 — same omission. Merged docstring
    documents challenge_mode at line 21 but not confirmation_mode. Belongs under
    ## Parameters (optional), next to challenge_mode.

  3. starkbank/merchantsession/__purchase.py:15 — same omission, and here it must go under
    ## Attributes (return-only), not ## Parameters (optional): the session-purchase endpoint does
    not accept confirmationMode as input (middlewares/merchantSessionPurchase.py:36).

  4. starkbank/merchantpurchase/__merchantpurchase.py:101delete() ships with no docstring.
    Every other public function in the module (create, get, query, page, update) has the
    standard ## Parameters (required) / (optional) / ## Return block. Needs one, stating that the
    operation is inferred from the purchase's status.

  5. starkbank/merchantpurchase/__merchantpurchase.py:93 — the update() docstring inherited
    from master says status [string, default None]: "canceled" or "reversed", per the rules above
    and its summary line covers only approved→cancel and confirmed→reverse. This PR's own README
    (## Confirm a MerchantPurchase) documents update(status="confirmed"), and the backend allows
    it. After the rebase the repo contradicts itself: the README teaches a status the docstring says
    does not exist. The update docstring must gain the confirmed case.

  6. tests/sdk/test_merchant_purchase.py — no test exercises the headline feature. The PR adds
    TestMerchantPurchaseDelete (queries status="approved", so it only ever hits the cancel path)
    and TestMerchantSessionCreateManualConfirmation, but nothing confirms a manual-mode purchase
    (update(status="confirmed", amount=...)) and nothing asserts confirmation_mode round-trips on
    a returned MerchantPurchase. A TestMerchantPurchaseConfirm is needed.

Non-blocking notes

  • README ## Cancel a MerchantPurchase says "Cancel an approved purchase or fully reverse a
    confirmed one" but the backend also reverses a paid purchase
    (middlewares/merchantPurchase.py:155). Worth a word.
  • CHANGELOG lists the confirmation_mode attribute and the delete method but not the
    update(status="confirmed") confirm path that the README now documents.
  • confirmation_mode is appended after updated in all three signatures and after tags in the
    assignment blocks; master's convention keeps created/updated last and the rest roughly
    alphabetical. Cosmetic.
  • No collision between the new TestMerchantPurchaseDelete (queries approved) and the existing
    TestMerchantPurchaseUpdate (queries confirmed).
  • Test class/util naming and the "query-then-loop, vacuously pass if empty" pattern match the
    existing file. Tree position of all 10 files is correct.

Two-line summary for the owner

Code and README are correct and line up with api-v2-ms-card-merchant (enum automatic/manual,
writable on MerchantSession and MerchantPurchase, return-only on MerchantSession.Purchase, confirm
via PATCH confirmed, bodyless DELETE inferring cancel/reverse); only CHANGELOG.md conflicts and
the rebase onto the 8 new master commits is a mechanical two-list concatenation.
Do not merge on the rebase alone: the branch forked before master's docstring pass and auto-merges
silently, so confirmation_mode would land undocumented on three classes, delete() with no
docstring, update()'s docstring denying the confirmed status its own README teaches, and the
confirm path untested — ask Paulo for those five doc lines plus a confirm test, then rebase and merge.

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.

3 participants