Group Activation

liveGroup activation by sequence range: a single request activates a whole run of cards at once via a two-phase prepare/execute flow.· 2026-05-30
0

The Two-Phase Flow

A single request activates a whole run of cards at once. The caller names the first and last card of a contiguous batch plus its sequence range; axis-service expands that into every activatable card in the range and processes them together. It runs in two phases — a synchronous prepare that validates and stages the group, then a deferred execute that drives the per-card Tribe activation. Most of the interesting logic lives in prepare, which puts every request through a gauntlet of structural and semantic checks — including the cross-batch guard that stops a range from quietly reaching across batch boundaries.

Service: axis-service · POST /api/cpm/v1/partners/{partnerExtId}/cards/multi/actions/{prepare|execute|findOne|cancel}

phase 1 · sync
prepare
Validate the gauntlet · expand the range · stage the group · returns actionGroupId, status New
→ ≤60s window →
phase 2 · sync
execute
Schedules the deferred task · status New → Processing · returns immediately
⇒ +100ms deferred ⇒
deferred task pool
GroupCardActionHandler
Walks the staged cards · one activation per card
→ 4-thread pool →
external
Tribe
activate / activateWithLoad per card
1

Selecting the Group

You don't enumerate every card. You give the first and last card of the batch (by id or external ref) and the sequence numbers that bracket them. axis-service resolves the membership: every activatable, same-design card whose sequence number falls in [seqLow, seqHigh]. cardCount declares how many you expect.

Request Body

json
{ "action": "activate", "clientRef": "batch-7741-2026", "operations": [{ "selector": { "cardIds": [firstId, lastId], "seqLow": 101, "seqHigh": 105 }, "cardCount": 5, "params": {} }] }

Selection Steps

StepTagDescription
1. Boundary shape validate() Selector must carry exactly 2 cardIds or 2 cardExtRefs -- the first and last card. Anything else is rejected before any DB work.
2. No pending conflicts database recognizeConflict() on each boundary card -- a card already inside an open action can't be re-grouped.
3. Endpoints match range database validateSeqNoMismatch() -- the first card's real sequence number must equal seqLow, the last card's must equal seqHigh.
4. Expand the range database findAllGroupCardIds(seqLow, seqHigh, designId, action) -- pulls every card of the first card's designId, in range, whose latest status is valid for the action. This set is the group.
2

Prepare — The Validation Gauntlet

Each gate is a check; failing one exits with a specific HTTP status + errorCode. Validation is layered: the validator layer (CardAccountActionValidator) checks the request's shape and provenance before any group is built; the task layer (GroupCardAccountActionTask.prepare) checks the resolved group against what the caller asked for.

Validator Layer — Request Shape & Provenance

GateCheckOn Failure
1. Selector present Selector carries cardIds / cardExtRefs. A group selector must name its boundary cards. 400 / 2020 -- missing selector params
2. Boundary array length Exactly first + last. Not 1, not 3. 412 / 2229 -- array size must be 2
3. Same program & design First and last card must be the same program and the same design -- the design is the batch's identity here. 417 / 2309 -- same program / designId
4. Boundary seqs match Each boundary card's stored sequence number must equal the range it claims. 417 / 2309 -- seqNumber mismatch
5. No overlap across ops Multi-op requests: no two ranges may intersect. 400 -- ranges overlap
6. Unique clientRef Idempotency guard -- a ref can be prepared once. 417 / 2230 -- clientRef not unique

Task Layer — Resolved Group vs Request

GateCheckOn Failure
7. Count matches validateCardCountMatchesRequest -- activatable cards found in range == the caller's cardCount. 417 / 2309 -- card count mismatch
8. Range is dense validateCrossBatchActivation -- (seqHigh - seqLow + 1) == cardsFound. The window must be a solid run -- no reaching across a gap into another batch. 417 / 2309 -- range != valid card count
9. Currency matches validateProgramCurrencyMatchesRequest -- for load actions, the requested currency must be the program's home currency. 417 / 2309 -- invalid currency
10. All cards resolve validateIfAllCardsHaveSameProgram -- each expanded id loads and belongs to the program; a missing id is a hard not-found. 404 -- card not found

Once all gates pass, an action_request (status New) is created, the group is written to group_card_action, and a New DTO with actionGroupId returns. A 120s timer is armed to cancel if execute never comes.

3

The Sequence-Range Guard

There is no batch identifier in the data. A batch is simply a block of cards minted together, which lands as a contiguous run of sequence numbers sharing one design_id. The same design can be reused for later mint runs, so its sequence space looks like dense islands separated by gaps — each island a batch. Sequence numbers are scoped per design, so the design_id filter in findAllGroupCardIds is what isolates the right design in the first place; this guard then enforces that the chosen range stays inside a single island.

How It Works

The check is: (seqHigh − seqLow + 1) == cardsFound. The window must be a solid run — no reaching across a gap into another batch.

ScenarioRangeWidthFoundResult
Activate batch one (solid island) seqLow=101, seqHigh=105 5 5 PASS -- 5 == 5
Reach across batches (gap at 106-109) seqLow=101, seqHigh=114 14 10 REJECT -- 14 != 10

The two checks work together. cardCount == found on its own would let a caller set cardCount to the 10 cards actually found and pass — silently grouping two batches and skipping whatever sits in the gap. Requiring width == found as well rules that out: the only way to satisfy both is a single gapless run.

4

Execute — Deferred Activation

execute schedules a deferred task and returns Processing at once; the per-card work runs later on a background worker. The deferral itself — the persisted queue, the scanner, the 4-thread pool, clientRef idempotency and the 60s/120s expiry timers — is the standard action mechanism. What follows is what GroupCardActionHandler does once it picks the group up.

Handler Steps

StepTagDescription
1. Fetch staged cards database Pull the group from group_card_action (status New) in pages of up to 100 and queue them. For load actions the whole batch amount is moved to a staging account first.
2. Activate on Tribe Tribe Per card: Cards.activateCard(spiCardId, ref) sets the card live on Tribe -- each card in its own transaction, so one card failing can't roll back the rest.
3. Record new status database addStatusUpdate() writes a card_update_activity row of Activated. If the program requires a KYC registration lock, the card is also flagged kyc_locked and pushed to Tribe status T (Suspended).
4. Settle card & action database Each group_card_action row goes Complete, or Failed with the error attached. When the pages drain, the action settles by completed-vs-total count. A batch with some failures is a normal Partial Complete; only an all-fail batch is Failed.

Card Status Per Action

ActionBeforeAfter (no KYC lock)After (KYC reg-lock)Tribe Calls
activate NOT_ACTIVATED ACTIVATED SUSPENDED (T) + kyc_locked activateCard [+ changeCardStatus T]
activateWithLoad NOT_ACTIVATED ACTIVATED + funds loaded SUSPENDED (T) -- lock holds the load activate + load [or KycLock]
5

Where Everything Ends Up

What Prepare Writes

EntityTableKey Details
action_request cpm One row per operation. Status New -> Processing -> Complete (or Canceled / Failed). access_scope_json holds the resolved cardIds + count. Carries the action type, program, clientRef.
group_card_action cpm One row per card in the group. Seeded from access_scope_json.cardIds. Per-card status 0 -> 100 as the handler claims them. Deleted on cancel / expiry.
entity_ext_ref cpm ClientRef linkage. ClientRef ref stored against the action. Backs the uniqueness gate (validator step 6).
payment_card_sequence_number cpm (read-only here) (card_id, design_id, seq_number). The source of truth the range guard reads. Written at provisioning, never by activation.

Supported Group Action Types

The same prepare/execute + sequence-range machinery serves a family of bulk actions. Each declares the card statuses it will accept (group-action-types.properties); the range guard applies to all of them.

Action TypeAccepted Starting Status
activate NOT_ACTIVATED
activateWithLoad NOT_ACTIVATED
load ACTIVATED
suspend ACTIVATED, RISK, LOST, FRAUD, SUSPEND
unsuspend SUSPENDED
markLost ACTIVATED, SUSPENDED, RISK, FRAUD, LOST
markStolen ACTIVATED, SUSPENDED, RISK, NOT_ACTIVATED, LOST, STOLEN
unmarkLost LOST