Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/Http/Controllers/SepaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,33 @@ public function paymentRequests(Request $request, Store $store): JsonResponse
}
}

/**
* Public NOP diagnostics ("Kde je moja platba") for one payment request.
* Only NOP-shaped references (QR- + 32 hex) can be looked up - anything
* else answers invalid_id here without a round trip to BTCPay.
*/
public function nopHistory(Store $store, string $reference): JsonResponse
{
if (preg_match('/^QR-[0-9a-fA-F]{32}$/', $reference) !== 1) {
return response()->json(['data' => [
'reference' => $reference,
'status' => 'invalid_id',
'environment' => 'PROD',
'message' => null,
]]);
}

$userApiKey = $this->ownerApiKey($store);

try {
$result = $this->sepaService->nopHistory($store->btcpay_store_id, $reference, $userApiKey);

return response()->json(['data' => $result]);
} catch (BtcPayException $e) {
return $this->handleBtcPayError($e);
}
}

public function confirmPaymentRequest(Store $store, string $reference): JsonResponse
{
$userApiKey = $this->ownerApiKey($store);
Expand Down
12 changes: 12 additions & 0 deletions app/Services/BtcPay/SepaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ public function listPaymentRequests(string $storeId, ?string $state = null, ?str
});
}

/**
* "Where is my payment": public NOP diagnostics timeline of a QR-
* payment request (plugin >= 0.8.0). Read-only; status found |
* not_found | invalid_id | unavailable.
*/
public function nopHistory(string $storeId, string $reference, ?string $userApiKey = null): array
{
return $this->client->withUserKey($userApiKey, function () use ($storeId, $reference) {
return $this->client->get($this->base($storeId).'/payment-requests/'.rawurlencode($reference).'/nop-history');
});
}

/**
* @return array { outcome }
*/
Expand Down
12 changes: 12 additions & 0 deletions docs/user/en/sepa-instant-qr.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ meta_description: Accept euro bank transfers via a SEPA Instant QR code, alongsi
2. Enter your bank details (the account that should receive the euro transfers) and enable the method.
3. Optionally set up e-mail confirmation of incoming payments so the store can reconcile them.

## Where is my payment

Every awaiting or needs-review payment whose reference starts with `QR-` has a **Where is my payment** button. It asks the public diagnostics service of the Slovak Financial Administration's instant payment notifier (NOP) what it knows about that reference and shows the timeline: transaction id created, bank notification stored, matched to the cash register, published, received.

What to expect:

- **NOP knows this id** appears for stores that confirm through NOP with an eKasa certificate, and for payments a notification-enabled bank account (Tatra banka, SLSP) reported.
- **NOP has not seen this id** is the normal answer for stores that confirm manually, through Fio or through e-mail: their references are generated locally, so NOP has nothing to show even when the money has already arrived (and even when Fio or e-mail confirmation has already settled the invoice). It does not mean the customer has not paid - check your bank account.
- The timeline never says which account was credited. Always check the transfer in your banking app before marking a payment as paid - satflux does not confirm anything from this screen.

The same data is on [kdejemojaplatba.sk](https://www.kdejemojaplatba.sk/), an independent viewer of the same service.

## Notes

- Available to all accounts, including guests.
Expand Down
12 changes: 12 additions & 0 deletions docs/user/sk/sepa-instant-qr.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ meta_description: Prijímajte eurové bankové prevody cez SEPA Instant QR kód,
2. Zadajte bankové údaje (účet, ktorý má prijímať eurové prevody) a zapnite metódu.
3. Voliteľne nastavte e-mailové potvrdzovanie prichádzajúcich platieb, aby ich obchod vedel spárovať.

## Kde je moja platba

Každá čakajúca platba alebo platba na kontrolu, ktorej referencia začína na `QR-`, má tlačidlo **Kde je moja platba**. Opýta sa verejnej diagnostiky Notifikátora okamžitých platieb (NOP) Finančnej správy SR, čo o tejto referencii vie, a ukáže časovú os: vznik ID transakcie, uloženie oznámenia banky, spárovanie s pokladnicou, sprístupnenie, prijatie.

Čo očakávať:

- **NOP toto ID pozná** sa zobrazí pri obchodoch, ktoré potvrdzujú cez NOP s eKasa certifikátom, a pri platbách, ktoré nahlásil notifikačný bankový účet (Tatra banka, SLSP).
- **NOP toto ID nevidel** je bežná odpoveď pri obchodoch s manuálnym, Fio alebo e-mailovým potvrdzovaním: ich referencie sa generujú lokálne, takže NOP nemá čo ukázať, ani keď peniaze už prišli (a ani keď Fio alebo e-mailové potvrdenie faktúru už uzavrelo). Neznamená to, že zákazník nezaplatil - skontrolujte bankový účet.
- Časová os nikdy nehovorí, na ktorý účet peniaze prišli. Pred označením platby ako zaplatenej ju vždy skontrolujte v bankovej aplikácii - satflux z tejto obrazovky nič nepotvrdzuje.

Rovnaké údaje ukazuje aj [kdejemojaplatba.sk](https://www.kdejemojaplatba.sk/), nezávislý prehliadač tej istej služby.

## Poznámky

- Dostupné pre všetky účty vrátane hostí.
Expand Down
61 changes: 60 additions & 1 deletion resources/js/__tests__/sepaPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,42 @@ const baseSettings = {
};

let settings = { ...baseSettings };
let nopHistoryStatus: "found" | "not_found" | "invalid_id" | "unavailable" = "found";

function primeApi({ available = true, overrides = {} as Record<string, unknown> } = {}) {
function primeApi({
available = true,
overrides = {} as Record<string, unknown>,
nopStatus = "found" as typeof nopHistoryStatus,
} = {}) {
settings = { ...baseSettings, ...overrides };
nopHistoryStatus = nopStatus;
apiMock.get.mockImplementation((url: string) => {
if (url.includes("/sepa/status")) {
return Promise.resolve({ data: { data: { available } } });
}
if (url.includes("/sepa/settings")) {
return Promise.resolve({ data: { data: settings } });
}
if (url.includes("/nop-history")) {
return Promise.resolve({
data: {
data: {
reference: "QR-ab29e346f1d841c8a95a63d857490818",
status: nopHistoryStatus,
environment: "PROD",
message: nopHistoryStatus === "unavailable" ? "NOP rate limit reached" : null,
createdAt: nopHistoryStatus === "found" ? "2026-09-16T08:00:00+00:00" : null,
indexedAt: nopHistoryStatus === "found" ? "2026-09-16T08:01:10+00:00" : null,
matchedAt: null,
publishedAt: null,
receivedAt: null,
organizationName: nopHistoryStatus === "found" ? "Kaviaren s.r.o." : null,
amount: nopHistoryStatus === "found" ? 12.5 : null,
currency: nopHistoryStatus === "found" ? "EUR" : null,
},
},
});
}
if (url.includes("/sepa/payment-requests")) {
return Promise.resolve({
data: {
Expand Down Expand Up @@ -142,6 +168,39 @@ describe("Sepa store page", () => {
expect(wrapper.text()).toContain("sepa.mark_paid");
});

it("opens the public NOP timeline for a QR- request", async () => {
primeApi();
const wrapper = await mountPage();
expect(wrapper.find('[data-testid="sepa-nop-history"]').exists()).toBe(false);

const button = wrapper.findAll("button").find((b) => b.text() === "sepa.nop_history_button");
expect(button).toBeDefined();
await button!.trigger("click");
await flushPromises();

const modal = wrapper.find('[data-testid="sepa-nop-history"]');
expect(modal.exists()).toBe(true);
expect(modal.text()).toContain("sepa.nop_history_found");
expect(modal.text()).toContain("sepa.nop_history_step_indexed");
expect(modal.text()).toContain("sepa.nop_history_disclaimer");
expect(apiMock.get).toHaveBeenCalledWith(
"/stores/store-1/sepa/payment-requests/QR-ab29e346f1d841c8a95a63d857490818/nop-history",
);
});

it("explains an unknown id instead of showing a timeline", async () => {
primeApi({ nopStatus: "not_found" });
const wrapper = await mountPage();

const button = wrapper.findAll("button").find((b) => b.text() === "sepa.nop_history_button");
await button!.trigger("click");
await flushPromises();

const modal = wrapper.find('[data-testid="sepa-nop-history"]');
expect(modal.text()).toContain("sepa.nop_history_not_found");
expect(modal.text()).not.toContain("sepa.nop_history_step_created");
});

it("shows the plugin-unavailable notice when the probe fails", async () => {
primeApi({ available: false });
const wrapper = await mountPage();
Expand Down
21 changes: 20 additions & 1 deletion resources/js/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,25 @@
"column_created": "Vytvořeno",
"mark_paid": "Označit jako zaplacené",
"payment_confirmed": "Platba označena jako zaplacená.",
"payment_confirm_failed": "Platbu se nepodařilo potvrdit"
"payment_confirm_failed": "Platbu se nepodařilo potvrdit",
"nop_history_button": "Kde je moje platba",
"nop_history_title": "Kde je moje platba",
"nop_history_close": "Zavřít",
"nop_history_loading": "Dotazujeme se NOP...",
"nop_history_failed": "Stav z NOP se nepodařilo načíst",
"nop_history_intro": "Veřejná diagnostika NOP slovenské Finanční správy (prostředí {environment}). Pouze pro čtení - nic se zde nepotvrzuje.",
"nop_history_found": "NOP toto ID transakce zná.",
"nop_history_amount": "Banka nahlásila {amount} {currency}.",
"nop_history_org": "Pokladna: {name}",
"nop_history_not_found": "NOP toto ID neviděl. To je běžné u obchodů bez NOP backendu: reference se generuje lokálně, takže NOP nemá co ukázat, ani když peníze už dorazily. Neznamená to, že zákazník nezaplatil - převod ověřte v bankovní aplikaci.",
"nop_history_invalid": "Tato reference není ID transakce NOP.",
"nop_history_unavailable": "NOP se teď nedá dotázat: {message}",
"nop_history_disclaimer": "Časová osa ukazuje cestu platby přes NOP, ne na který účet přišla. Před označením jako zaplacené převod zkontrolujte v bankovní aplikaci.",
"nop_history_step_created": "Vznik ID transakce",
"nop_history_step_indexed": "Oznámení banky uloženo",
"nop_history_step_matched": "Spárováno s pokladnou",
"nop_history_step_published": "Zpřístupněno pokladně",
"nop_history_step_received": "Přijato pokladnou",
"nop_history_pending_step": "zatím ne"
}
}
21 changes: 20 additions & 1 deletion resources/js/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,25 @@
"column_created": "Erstellt",
"mark_paid": "Als bezahlt markieren",
"payment_confirmed": "Zahlung als bezahlt markiert.",
"payment_confirm_failed": "Zahlung konnte nicht bestätigt werden"
"payment_confirm_failed": "Zahlung konnte nicht bestätigt werden",
"nop_history_button": "Wo ist meine Zahlung",
"nop_history_title": "Wo ist meine Zahlung",
"nop_history_close": "Schließen",
"nop_history_loading": "NOP wird abgefragt...",
"nop_history_failed": "Der NOP-Status konnte nicht geladen werden",
"nop_history_intro": "Öffentliche NOP-Diagnose der slowakischen Finanzverwaltung (Umgebung {environment}). Nur lesend - hier wird nichts bestätigt.",
"nop_history_found": "NOP kennt diese Transaktions-ID.",
"nop_history_amount": "Eine Bank hat {amount} {currency} gemeldet.",
"nop_history_org": "Kasse: {name}",
"nop_history_not_found": "NOP hat diese ID nicht gesehen. Das ist normal für Shops ohne NOP-Backend: die Referenz wird lokal erzeugt, daher zeigt NOP nichts, auch wenn das Geld bereits eingegangen ist. Es bedeutet nicht, dass der Kunde nicht bezahlt hat - prüfen Sie die Überweisung in Ihrer Banking-App.",
"nop_history_invalid": "Diese Referenz ist keine NOP-Transaktions-ID.",
"nop_history_unavailable": "NOP kann gerade nicht abgefragt werden: {message}",
"nop_history_disclaimer": "Die Zeitleiste zeigt den Weg der Zahlung durch NOP, nicht welches Konto gutgeschrieben wurde. Prüfen Sie die Überweisung in Ihrer Banking-App, bevor Sie sie als bezahlt markieren.",
"nop_history_step_created": "Transaktions-ID erstellt",
"nop_history_step_indexed": "Bankbenachrichtigung gespeichert",
"nop_history_step_matched": "Der Kasse zugeordnet",
"nop_history_step_published": "Für die Kasse bereitgestellt",
"nop_history_step_received": "Von der Kasse empfangen",
"nop_history_pending_step": "noch nicht"
}
}
21 changes: 20 additions & 1 deletion resources/js/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,25 @@
"column_created": "Created",
"mark_paid": "Mark as paid",
"payment_confirmed": "Payment marked as paid.",
"payment_confirm_failed": "Failed to confirm the payment"
"payment_confirm_failed": "Failed to confirm the payment",
"nop_history_button": "Where is my payment",
"nop_history_title": "Where is my payment",
"nop_history_close": "Close",
"nop_history_loading": "Asking NOP...",
"nop_history_failed": "Could not load the NOP status",
"nop_history_intro": "Public NOP diagnostics of the Slovak Financial Administration ({environment} environment). Read-only - nothing is confirmed here.",
"nop_history_found": "NOP knows this transaction id.",
"nop_history_amount": "A bank reported {amount} {currency}.",
"nop_history_org": "Cash register: {name}",
"nop_history_not_found": "NOP has not seen this id. That is expected for stores without a NOP backend: the reference is generated locally, so NOP has nothing to show even when the money has already arrived. It does not mean the customer has not paid - check the transfer in your banking app.",
"nop_history_invalid": "This reference is not a NOP transaction id.",
"nop_history_unavailable": "NOP could not be asked right now: {message}",
"nop_history_disclaimer": "The timeline shows the payment's journey through NOP, not which account was credited. Check the transfer in your banking app before marking it as paid.",
"nop_history_step_created": "Transaction id created",
"nop_history_step_indexed": "Bank notification stored",
"nop_history_step_matched": "Matched to the cash register",
"nop_history_step_published": "Published to the cash register",
"nop_history_step_received": "Received by the cash register",
"nop_history_pending_step": "not yet"
}
}
21 changes: 20 additions & 1 deletion resources/js/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,25 @@
"column_created": "Creado",
"mark_paid": "Marcar como pagado",
"payment_confirmed": "Pago marcado como pagado.",
"payment_confirm_failed": "No se pudo confirmar el pago"
"payment_confirm_failed": "No se pudo confirmar el pago",
"nop_history_button": "Dónde está mi pago",
"nop_history_title": "Dónde está mi pago",
"nop_history_close": "Cerrar",
"nop_history_loading": "Consultando NOP...",
"nop_history_failed": "No se pudo cargar el estado de NOP",
"nop_history_intro": "Diagnóstico público de NOP de la Administración Financiera eslovaca (entorno {environment}). Solo lectura - aquí no se confirma nada.",
"nop_history_found": "NOP conoce este ID de transacción.",
"nop_history_amount": "Un banco informó {amount} {currency}.",
"nop_history_org": "Caja registradora: {name}",
"nop_history_not_found": "NOP no ha visto este ID. Es normal en tiendas sin backend NOP: la referencia se genera localmente, así que NOP no muestra nada aunque el dinero ya haya llegado. No significa que el cliente no haya pagado - verifica la transferencia en tu app bancaria.",
"nop_history_invalid": "Esta referencia no es un ID de transacción de NOP.",
"nop_history_unavailable": "No se pudo consultar NOP ahora mismo: {message}",
"nop_history_disclaimer": "La línea de tiempo muestra el recorrido del pago por NOP, no a qué cuenta se abonó. Comprueba la transferencia en tu app bancaria antes de marcarla como pagada.",
"nop_history_step_created": "ID de transacción creado",
"nop_history_step_indexed": "Notificación bancaria guardada",
"nop_history_step_matched": "Emparejado con la caja",
"nop_history_step_published": "Publicado para la caja",
"nop_history_step_received": "Recibido por la caja",
"nop_history_pending_step": "todavía no"
}
}
21 changes: 20 additions & 1 deletion resources/js/locales/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -4540,6 +4540,25 @@
"column_created": "Vytvorené",
"mark_paid": "Označiť ako zaplatené",
"payment_confirmed": "Platba označená ako zaplatená.",
"payment_confirm_failed": "Platbu sa nepodarilo potvrdiť"
"payment_confirm_failed": "Platbu sa nepodarilo potvrdiť",
"nop_history_button": "Kde je moja platba",
"nop_history_title": "Kde je moja platba",
"nop_history_close": "Zavrieť",
"nop_history_loading": "Pýtame sa NOP...",
"nop_history_failed": "Stav z NOP sa nepodarilo načítať",
"nop_history_intro": "Verejná diagnostika NOP Finančnej správy SR (prostredie {environment}). Len na čítanie - nič sa tu nepotvrdzuje.",
"nop_history_found": "NOP toto ID transakcie pozná.",
"nop_history_amount": "Banka nahlásila {amount} {currency}.",
"nop_history_org": "Pokladnica: {name}",
"nop_history_not_found": "NOP toto ID nevidel. To je bežné pri obchodoch bez NOP backendu: referencia sa generuje lokálne, takže NOP nemá čo ukázať, ani keď peniaze už prišli. Neznamená to, že zákazník nezaplatil - prevod overte v bankovej aplikácii.",
"nop_history_invalid": "Táto referencia nie je ID transakcie NOP.",
"nop_history_unavailable": "NOP sa teraz nedá opýtať: {message}",
"nop_history_disclaimer": "Časová os ukazuje cestu platby cez NOP, nie na ktorý účet prišla. Pred označením ako zaplatené prevod skontrolujte v bankovej aplikácii.",
"nop_history_step_created": "Vznik ID transakcie",
"nop_history_step_indexed": "Oznámenie banky uložené",
"nop_history_step_matched": "Spárované s pokladnicou",
"nop_history_step_published": "Sprístupnené pokladnici",
"nop_history_step_received": "Prijaté pokladnicou",
"nop_history_pending_step": "zatiaľ nie"
}
}
Loading
Loading