Skip to main content

Pinpad app (unpispas-ptf)

Staff-facing SPA for payment terminals (datáfonos). Operators sign in, pick a place, and work from a mobile-first home: Mesas, Avisos, Tickets, optional Caja, and cart with the same product selector as POS mobile (upp-cart-select). This page covers access (epic 01), payment-terminal linking (epic 02), the work screen (epic 03), card charge on the terminal (epic 04), what happens when the charge goes wrong (epic 05), printing the receipt on the terminal (epic 06), automatic receipt on payment (epic 07), cash block (epic 08), card refunds on the terminal (epic 09), charging guest payment requests from a table (epic 10), and charge from the POS till (epic 11).

Floor guides (Spanish, no API names): staff · manager. Print-service wire contract and handoff for pispas-connect: Print-service requirements.

LayerWhere
App shellapps/unpispas-ptf/ — Access = 'LOGIN', WorkMode = 'PTF', AppProfile = { mode: 'mobile', zoom: 0.9 }
Screenslibs/feature/pinpad/upp-feature-pinpad.mdc (home, work, tables, tickets, cash, cart)
Login / userReuses feature-login / feature-user (no feature-place / feature-guest)
Sync (PTF)Shared floor-first priorities (RN-SYNC-30); client=PTF → server datamodel target pinpad (no cash-control transfer, RN-SYNC-31); refresh ×4; Start awaits create — sync.mdc RN-SYNC-30…33
RESTPOST /login?client=PTF (via adhoc WorkMode) marks SESSION.mode; device-name clash → Exists (8) — OpenAPI in docs/swagger/
Terminals (POS)libs/feature/devices — PINPAD under each print service

Canonical URL​

SurfaceHostPublic path
Nx dockerptf.nx.{domain}/unpispas-ptf/
DEV/LAB edgeptf.{DOMAIN} (PTF_DOMAIN)Host / (own ACME cert, not in api+app SAN). Legacy /unpispas-ptf/* 301 to that Host.

Production baseHref is /; development keeps /unpispas-ptf/. Materialize writes appPath: 'ptf.' and appRoute: '/' for this app. Opening app.* on a datáfono does not redirect to ptf (RN-PTA-03).

Staff flow​

StepBehaviour
LoginAlways remembers session; device name field uses shared @login_deviceid_* (PTF catalog customizes copy; name is display / fingerprint disambiguation only, not a unique id). Query client=PTF from adhoc. Splash uses the same MarkReady paint delay as POS (500 ms).
PlaceAuto-enter when the user has a single place; otherwise picker.
Home / workMobile chrome; work tabs Mesas / Avisos (badge = pending PayRequests) / Tickets; Caja only with TillManage and exactly one till (hydrate on first open). Settings zoom 50–100%; change place if multi-place; logout confirm. Ticket detail and cart follow the POS mobile panel pattern: one shell upp-panel; header/tabs hide while ticket/cart fill panel-content (same idea as place/upp-view / upp-cart / QRA order layers).
WarningsUnlinked when the session PosDevice has no linked terminal (!Linked); “does not look like a datáfono” via platform.SeemsMobile — never block navigation.

Payment terminals (epic 02)​

Terminals are registered in the usual POS app (app.*), under each print service’s device editor — never inside ptf.* (RN-PTF-09).

TopicBehaviour
Wherefeature/devices → service editor → «Terminal de pago» (upp-pinpad-edit / upp-pinpad-view)
DataName, host, port (default 8887), optional write-only API key, oncash / oncard / nocash
PersistPINPAD.posdevice → POSDEVICE.objid (cross-DB, same pattern as RASPPICONNECT / SESSION)
Link UIChoose a PosDevice from PTF sessions (place.sessions) plus already-linked devices (pinpads); selection and exclusion use instance identity (===); one active device per place
Presence«Connected» when the row’s mode=PTF session is IsActive (AC + touch ≤ 10 min)
On ptfUppPinpadHomeComponent.Linked when session.device.pinpad returns a terminal

Model detail: data-objects — Pinpad. Rules: upp-model.mdc (RN-DIL-* / RN-PTM-*), upp-feature-devices.mdc, upp-feature-pinpad.mdc (RN-PTF-06).

Card charge (epic 04)​

When the place has active payment terminals and this datáfono is linked (PlaceView.CardMethod === 'TERMINAL'), Tarjeta opens upp-pinpad-payment (owned by feature/pinpad; ticket hosts open the dialog) against PlaceView.OwnPinpad via paytefService. Unlinked devices show @ticket_cardterm_unlinked and do not open a dialog.

Charge from the POS till (epic 11)​

On the till app (unpispas-pos), if the place has ≥1 active PINPAD, CardMethod is also TERMINAL and Tarjeta opens the same Paytef dialog against a terminal resolved from inventory (OwnPinpad is null on POS — so cash blocking and auto-receipt stay off on the till). One terminal is used without asking; several open upp-pinpad-select on every payment (rows from BuildOptions: till label + connected badge, informational only — no session memory of the last choice). Card returns use the same three options as on ptf; an unbound refund picks via place.pinpads / the same picker. Without a BAR printer, fiscal receipts can go through a terminal on the till service (place.service.pinpads, resolved by the print queue). Confirming a guest table PAYCARD request (RequestDone) still registers with DoPayment (no pinpad UI in feature/qrcode — Nx isolation).

What the waiter seesBehaviour
StagesConnecting → waiting card → PIN / currency / signature / customer → processing (RF-04-05 texts).
Approved«Aprobado», auto-close ≈1.5 s, ticket closed with PinpadCharge.Settle → ApplyPinpad (no accept button).
CancelDisables cancel, shows «Cancelando el cobro»; if the terminal already approved → hint «ya se había aprobado».
Failure (clean / DE)The reason in its own words, with Reintentar, Marcar como pagado and Cerrar (see When the charge goes wrong).
Uncertain (PE / PENDING)Status-first: WAITING polls Resolve (5 s, up to 90 s of UNKNOWN; IN_PROGRESS does not burn the budget). No Reintentar. Idle → Consultar / Manual / X (no new charge while the terminal is unreachable).
Escape / outsideIgnored while the charge dialog is open.
Reopen with PEPoll Resolve (same as uncertain); if still unknown → idle / persistent «pendiente de comprobar» with ⋯ unlock. Tarjeta stays enabled but the dialog never charges until the prior attempt is clear or discarded.

Ticket data stored (PINPADPAY): reference, amounts, terminal codes, auth, timestamps, receipts, signature, status PE/AP/DE/MM/MA. Closing the ticket is only via TicketView.ApplyPinpad() after an approved (AP) or manually declared (MA) charge.

When the charge goes wrong (epic 05)​

Clean failures (DE) leave the dialog open with the reason and Retry / Manual / Close. Uncertain results (PE) do not offer Retry: the panel polls terminal status first (Adyen/MarketPay pattern). Nothing closes by itself and nothing is charged twice to “resolve doubt”.

ExitWhat it does
ReintentarStarts a new charge for the same amount only when the previous attempt is known not charged. Not offered while PE remains, nor on amount mismatch.
Consultar de nuevoFrom idle: restarts the Resolve poll on the same attempt.
Marcar como pagadoRecords that the waiter took the payment by other means and closes the ticket as paid by card (with confirmation if a PE is still unresolved).
CerrarLeaves the ticket unpaid. An attempt that could not be checked stays pending and is checked when the ticket is reopened. Discard without a new charge is only from the ticket ⋯ unlock menu (Supersede).

The messages below are indicative wording (final texts are agreed with the customer). The app never guesses the reason from free text: it shows the one the print service reports, and the generic message when none is reported.

SituationMessage (es)
No payment service on this deviceNo hay servicio de cobro
No connection with the terminalSin conexión con el datáfono
Terminal not operationalEl datáfono no está operativo
Paytef service unavailableEl servicio del datáfono no está disponible
Operation not authorisedEl datáfono no autoriza la operación
Terminal busy (another device or this one)El datáfono está ocupado
Charge could not startNo se pudo iniciar el cobro
State of the charge cannot be readNo se pudo consultar el estado del cobro
Three minutes without payingTiempo de espera agotado
No result obtainedNo se obtuvo el resultado del cobro
Cancelled on the terminalCobro cancelado en el datáfono
DeclinedPago denegado
Terminal charged another amountEl importe cobrado no coincide
  • The earlier attempt is checked before repeating. If the customer already paid but the dialog missed it, the dialog shows it as approved instead of charging again; if the terminal cannot be asked, the waiter must confirm explicitly.
  • «Marcar como pagado» is a declaration of the waiter. It is stored as a manual payment (status MA), without terminal receipts, and closes the ticket exactly like an approved charge.
  • While the terminal waits, Cancelar asks the terminal to cancel, and Marcar como pagado cancels first and then declares the payment, unless the terminal had already approved it. If the terminal does not answer the cancellation, the dialog closes anyway with a warning and the charge is checked when the ticket is reopened.
  • Two charges at once are not possible on the same ticket or the same terminal from this app: the second shows «El datáfono está ocupado» and is not queued.

Handoff for pispas-connect. The Nx app (unpispas-ptf / POS) already speaks the 13-point wire contract. Closing the Payteff plan on the app side leaves the print-service gaps below as the next owned work in pispas-connect (desktop paytef.rs + Android PaytefService.java). Publish the durable behaviour write-up as pispas-connect/docs/PAYTEF.md and link it from MODULES.md. Checklist contrasted on 2026-10-24 (code + unit tests; no real pinpad in that pass).

#NeedDesktopAndroid
1Order language es/en/eu reaches the pinpadnono
23 min ceiling: cancel and read final resultno (5 min, no final read)no (no ceiling)
3Last-result query (op, ref, amount, time, free/busy)nono
4Refund with caller-supplied original operation numberno (24 h local cache only)no
5Typed failure causeno (generic error string)no
6Full result (tip, original amount, returned ref, receipts)no (partial payload)no
7Ask for receipts; no tips; no pinpad receipt options; keep signatureno (createReceipt: false)no
8Busy: reject without queueing, report busynono
9Print success from printer result; query presence/widthno (HTTP 2xx = success)no
10Honour terminal host/portyesno (always 127.0.0.1:8887, by design on-device)
11Optional API key sent when presentnono
12Forward transaction/card status as-isyesyes
13CANCEL on another connection during SALE; answer PING on the waiting connectionunproven / PING blockedunproven / PING blocked

Priority for connect: 2, 3, 5, 7, 8, 9, 13 (busy + last-result + cancel life-cycle unblock CA that the app already implements). Client transport: WSConnection with timeoutMs, suspended keep-alive, and OnProgress. Charge failures without a typed cause show the generic message; print without a printed flag surfaces as UNKNOWN / browser backup.

On ptf, Imprimir on a ticket does not open the browser dialog first. Destination order (owned by queueService):

  1. BAR printers of the till service (same as POS).
  2. Else the own Paytef terminal (place.OwnPinpad) — same print path as BAR; the fiscal QR is already on the paid ticket.
  3. Else the browser print dialog.
OutcomeWhat the operator sees
Printed on terminal or BARNothing (silent). History gets one RECEIPT.
Terminal unreachable / offline«Sin conexión con el terminal», then browser backup. No RECEIPT.
Terminal busy«El datáfono está ocupado», then browser. No RECEIPT.
No printer on terminal«Este datáfono no tiene impresora», then browser. No RECEIPT.
Other print failure«No se pudo imprimir en el terminal», then browser. No RECEIPT.
  • Fiscal QR: produced at payment, before any print job. The terminal printer is equivalent to a BAR printer: it prints the ticket as-is (QR included when the place emits VeriFactu/TicketBAI).
  • Paying on ptf never opens the cash drawer. The receipt is printed on request with Imprimir, or automatically when the option described below is on.
  • Card voucher: menu option «Imprimir comprobante de tarjeta» when a stored merchant/customer copy exists and there is an own terminal. Chooses copy if both exist; terminal only (no BAR/browser, no RECEIPT).

Automatic receipt on payment (epic 07)​

Each payment terminal has two switches, edited in the POS app (app.*) under the print service's «Terminal de pago» editor: «Imprimir recibo al cobrar con tarjeta» (oncard) and «Imprimir recibo al cobrar en efectivo» (oncash). With one on, the receipt comes out on the datáfono by itself when the waiter charges in that modality from ptf.

SituationWhat happens
Card charge approved, or Marcar como pagado, with oncard onOne receipt on the datáfono.
Cash payment with oncash onOne receipt on the datáfono. The drawer is not opened.
Option off for that modalityNothing is printed; Imprimir keeps working as before.
Refund, mixed payment, account payment, partial payment, account settlementNo automatic receipt.
Several payments in a rowOne receipt per ticket, in charge order, one at a time; a failed one does not stop the next.
Screen closed right after chargingThe receipt is still printed.
Datáfono busy right after the saleRetried up to 3 times, 2 s apart (assumed values, to be confirmed in the pilot); then the «ocupado» warning.
No paper / unreachable / other failureThe warning with the cause. No browser dialog and no RECEIPT; the payment stays correct and Imprimir retries.
  • The automatic receipt goes only to the own terminal, even if a BAR printer is configured; the POS app is unchanged.
  • The charge never waits for the paper: printing runs in the background.

Cash block (epic 08)​

Each payment terminal has «No permitir efectivo» (nocash), edited in the POS app under the print service's «Terminal de pago» editor. When it is on for the own terminal of a ptf session, that datáfono no longer offers cash collection.

SituationWhat happens
Own terminal with nocash onCash button (ticket and list), «change to cash», mixed and partial disappear.
Same ticket on the POS tillCash stays available (place.CanCash ignores nocash outside ptf).
Option turned off, then syncptf offers cash again, including with a ticket already open on screen.
Two datáfonos, only one with nocashEach applies its own terminal's option.
Datáfono without a linked terminalBlock does not apply (place.CanCash stays on place option); card charge still needs a link (epic 04).
Ticket already paid in cashHistory, payment and amounts stay unchanged.
Cash refundsNot changed by this epic (see refunds epic).
  • Tarjeta and account charge keep the same routes.
  • No new warnings or language keys: blocked actions are simply not shown.
  • A cash choice already pending locally (_pendpay) is not revoked when the option flips mid-flow.

Card refunds (epic 09)​

When a closed ticket has a card part to give back and this datáfono is linked (PlaceView.CardMethod === 'TERMINAL'), returning products with a card amount reuses the same upp-pinpad-payment dialog in refund mode instead of just adjusting the ticket. The waiter decides how, the app decides which terminal.

What the waiter seesBehaviour
No terminal involvedCash-only, TPV without a terminal, or CardMethod === 'UNLINKED': the return registers exactly as before epic 09.
Linkable refundA full refund of a single approved card charge (not a mixed/manual one, no prior refund of it, not part of a multi-order account charge) offers the sale's own terminal, even if this datáfono is a different one.
Loose refundA partial card refund, or one that cannot be linked, offers this datáfono's own terminal.
Sale's terminal unreachableOnly Marcar como pagado and Cancelar; the terminal dialog never opens.
Cancel at the choiceNothing changes: no OnReturn, no commit, no new PINPADPAY row.
Approved on the terminal, or declared manuallyThe return registers (OnReturn + commit), then the refund row is marked applied. A failed or cancelled attempt leaves the ticket untouched.
Already-approved refund of the same amountRegisters straight away without asking or reaching the terminal again (a known limit: a later legitimate refund of the same exact amount would also be absorbed by that leftover row until it is consumed).
Cash blocked on this terminal (epic 08)The cash refund option disappears; a full card refund is still offered.
Reopening with a refund pendingSame persistent-notice pattern as a pending charge, checked first: pending card charge, then pending refund, then an approved-but-unregistered refund, then nothing.

The approved path, end to end:

  • No automatic receipt, no reprint. A refund never triggers the automatic-receipt option (epic 07) and this epic does not offer a voucher reprint for it.
  • No two card operations at once. A charge and a refund never run together on the same ticket or the same terminal from this app.
  • Terminal first, ticket after. The ticket is only touched (OnReturn + commit) once the terminal has approved — or the waiter declared it manually — never before.

Table payment requests (epic 10)​

Guests on qra still only request payment (cash/card AskWaiter + IsToPay). They never run Paytef. On ptf, pending asks surface like POS: the shared topbar WAITER strip (place.AlertSize / AlertTables) and Attention on the mesa thumb. There is no Avisos footer tab. Tapping the topbar strip (single mesa) or an Attention mesa opens upp-table-edit (ventana de cobro). Work remaps topbar placetables onto the Mesas tab.

What the waiter seesBehaviour
Topbar WAITERSame strip as POS; one mesa → open that table Attention; several → Mesas tab with Attention thumbs.
Mesa with AttentionTap opens upp-table-edit cobro (POS parity).
Charge card / cashSame ticket / Attention payment flows as POS (upp-pinpad-payment, cash when CanCash).
Fail / cancelTickets stay IsToPay; asks stay open; topbar / Attention remain.
Later refund of one billJoint-charge rows with Orders.length > 1 stay loose refunds (epic 09 / RF-09-09b).
Auto receiptOne automatic receipt per paid ticket (epic 07).

Helper: upp-pinpad-paycharge for joint-charge orchestration when needed. Model: PayRequest / PlaceView.PayRequests / AlertTables (upp-model.mdc RN-ASK-PAYREQ-*). Service: multi-ticket Charge / incidents (upp-paytef-service.mdc).

Assumptions checked in the pilot (S-PTF-*)​

Conservative working assumptions. A row is only relaxed when a real Paytef pinpad confirms otherwise. The Nx Payteff plan is closed (2026-10-31): field pilot and customer sign-off stay outside this closure (connect team + local acceptance). Until that field pass, every row stays Not yet run.

IDAssumption still in forceField result
S-PTF-01Linked refund only for the full amount of that charge and with no prior refunds; partials, later refunds and one bill of a joint charge go looseNot yet run on a real pinpad
S-PTF-02Charge reference ≤ 32 characters, no specials; joint charge = first order + +NNot yet run
S-PTF-03Cancel does not guarantee “not charged”: the final result is read; if approved, the ticket is chargedNot yet run
S-PTF-04Card receipts may be required; the app asks for them and stores them. The current print service sends createReceipt: falseNot yet run
S-PTF-05API key optional on the terminal; HTTPS out of this plan. The current print service never sends the keyNot yet run
Charge in the 5 s result window (epic 04 scenario 10)Whether the pinpad accepts a new sale or answers busyNot yet run

Isolation​

  • stateService.WorkMode === 'PTF' drives client behaviour; POS stays POS.
  • No imports of feature-place / feature-guest from feature-pinpad or the ptf shell.
  • Session mark SESSION.mode drives presence/linking; guests get mode = QRA.

Contracts​

  • App: apps/unpispas-ptf/unpispas-ptf.mdc
  • Feature: libs/feature/pinpad/upp-feature-pinpad.mdc
  • Devices: libs/feature/devices/upp-feature-devices.mdc
  • Floor guides: ptf-personal, ptf-responsable
  • Print service handoff: table above → implement + document in pispas-connect (docs/PAYTEF.md, MODULES.md); client contract libs/feature/drivers/src/lib/upp-paytef-service.mdc
  • View profile: libs/upp-base/src/frontlib/view.mdc (AppProfile)
  • Login / device-name Exists: server/src/App/Services/login/login.mdc, libs/feature/login/.../upp-login.mdc
  • Deploy Host: unpispas.deploy PTF_DOMAIN (parity LAB/DEV with QRA_DOMAIN pattern)