Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Expresní SMS platby

Expresní SMS platby#

Expresní SMS zaplatí jednu službu jedinou zprávou KLÍČOVÉ_SLOVO MODUL SLUŽBA ID, bez platebního formuláře. Platba v okamžiku příchodu SMS neexistuje — vznikne až z ní a pak běží stejným zpracováním jako platba z formuláře. Pro provozovatele je totéž v Platba SMS.

Části#

Část Kde Úkol
ExpressSmsParser app/Core/Payments/Express/ parse() text → ExpressSmsCommand (expresní / formulářová / neplatná), compose() týž tvar pro front, serviceCodeFromKeyword()
ExpressServiceProviderInterface tamtéž jeden modul: getModuleCode(), resolve(), createPaymentItems(), describe()
ExpressServiceRegistry tamtéž slovo modulu → provider; provideři se hledají líně podle typu v DI kontejneru; describe(gateId) doplní ke službám číslo brány za jejich cenu
AbstractQuickPriceExpressServiceProvider tamtéž společná logika modulů s rychlými cenami (Bazaar, Comcat, Discussion)
ExpressTarget, ExpressResolution, ExpressServiceDescription tamtéž DTO mezi modulem a System vrstvou
ExpressModuleCode tamtéž slova modulů (INZ, FIRMA, DISK, UCET) — sdílí je Api i Front
PaymentExpressExecuteTrait app/UI/Api/System/Presenters/Traits/Actions/ příjem SMS api/system/payment/express-execute
PaymentExpressOffersTrait tamtéž interní akce expressOffers — text a číslo pro front
ExpressPaymentTrait app/Core/Traits/Front/Presenters/ sekce „Expresní platba“ u objektu na frontu

Provideři dnes:

Třída Slovo Služby Callback
Api\Bazaar\Payments\BazaarExpressServiceProvider INZ rychlé ceny tarifů inzerátu BAZAAR_PAYMENT
Api\Comcat\Payments\ComcatExpressServiceProvider FIRMA rychlé ceny tarifů firmy COMCAT_PAYMENT
Api\Discussion\Payments\DiscussionExpressServiceProvider DISK rychlé ceny tarifů příspěvku DISCUSSION_PAYMENT
Api\System\Payments\MembershipExpressServiceProvider UCET CLEN (tarify s duration > 0), KREDIT (tarify s credits > 0), ceny tarifů pro bránu SYSTEM_USER_PAYMENT

Tabulka „expresních služeb“ v databázi záměrně není: službami jsou rychlé ceny (cms_mod_*_tariff_quick_prices), resp. ceník tarifů členství. Druhý seznam by se od nich tiše rozjel.

Průběh příjmu SMS#

internalActionExpressExecute() v PaymentExpressExecuteTrait:

  1. Brána z parametru pg (prázdné = mobilniplatby); musí implementovat ExpressSmsGatewayInterface.
  2. Původ: verifyNotificationOrigin() — sdílené tajemství delivery_secret, fail-closed. Chybějící id nebo shortcode je odmítnutí. Odmítnutí nic nezakládá ani nemění, zapíše jen log expressExecute bez payment_id.
  3. Idempotence: platba se stejným gateway_payment_id u téže brány → vrátí se uložená původní odpověď a nic se nezpracuje.
  4. Parse. Tvar KLÍČOVÉ_SLOVO <kód> je formulářová SMS: loadPayment() → checkPayment() → processPayment().
  5. Expresní SMS: registry->get(modul) → provider->resolve(služba, id, gateId).
  6. Cena výhradně z PaymentGatePhone podle shortcode; vybere se cíl, jehož cena a měna se rovnají číslu (matchExpressPrice()). Cílů je víc, když službu nabízí víc regionů nebo — u členství — víc cen téhož tarifu.
  7. Založí se Payment (cena a měna čísla, user_id + e-mail vlastníka, callback, gateway_payment_id, region), účetní snapshot cms_system_payment_items a položky modulu přes createPaymentItems() — neaktivní.
  8. processPayment() se syntetickým GatewayCheckResult: transakce, stav, callback modulu (aktivace), PAYMENT_DONE, doklad cronem. MO číslo (hladina 0) = COMPLETED, MT = PENDING a dokončí ho doručenka.
  9. Odpověď: text/plain; charset=utf-8, HTTP 200, explicitní Content-Length, ASCII text; u MT ;<hladina>.

Rozhraní provideru#

interface ExpressServiceProviderInterface
{
    public function getModuleCode(): string;
    public function resolve(string $serviceCode, int $objectId, int $paymentGateId): ExpressResolution;
    public function createPaymentItems(ExpressTarget $target, int $paymentId): bool;
    /** @return list<ExpressServiceDescription> */
    public function describe(int $paymentGateId): array;
}
  • resolve() dostává neověřený vstup od kohokoli: službu z textu a id. Musí ověřit, že služba je teď nabízená a objekt existuje a je aktivní. Vrací ExpressResolution::found([...ExpressTarget]), nebo failed() s důvodem unknownService / objectNotFound / objectInactive.
  • ExpressTarget nese tarif, cenu a měnu služby, region, callbackKey, vlastníka (ownerUserId, ownerEmail), allowDuplicateActivation, typ produktu a druh plnění pro doklad. Cenu z textu SMS v něm nikdy nemá.
  • $paymentGateId využije modul, jehož ceny se liší podle brány (tarify členství mají cenu na řádku region × brána × měna). Moduly s rychlými cenami ho ignorují.
  • createPaymentItems() založí položky neaktivní přes manager modulu (s jeho save hooky); aktivuje je callback, až je platba COMPLETED.
  • describe() vydá nabízené služby pro admin přehled a pro front. Číslo k nim doplní registr.

Postup: přidat expresní službu modulu s rychlými cenami#

Modul má tarify s rychlými cenami (tarif → rychlá cena na region, položka platby = objekt + tarif + platba), jako Bazaar, Comcat a Discussion.

  1. Do ExpressModuleCode přidejte slovo modulu (velká písmena, bez diakritiky).
  2. V Api vrstvě modulu založte app/UI/Api/<Modul>/Payments/<Modul>ExpressServiceProvider.php jako final class … extends AbstractQuickPriceExpressServiceProvider. Konstruktor bere typované managery modulu (objekt, tarif, položka platby).
  3. Implementujte getModuleCode(), getCallbackKey(), buildObjectLabel() (ASCII, do SMS), createPaymentItemEntity(), findObject(), findActiveTariffs() (s translations a quickPrices) a savePaymentItem().
  4. Když objekt není inzerát, přepište háčky:
  5. isObjectActive() — výchozí je isActive() entity; příspěvek diskuze bere !banned && !deactivated,
  6. bindPaymentItemObject() — výchozí je setAdvertId(); diskuze volá setMessageId(). Smazaný objekt (isDeleted()) odmítá základ sám, vlastníka čte z getOwnerUserId().
  7. Callback klíč musí být v PaymentCallbackRegistry::HANDLERS a mít řádek v cms_system_payment_callbacks — bez něj skončí každá SMS jako callbackMissing.
  8. Registrace v DI není potřeba: třída leží v app/UI, kterou prochází search, a registr ji najde podle rozhraní. Po přidání třídy smažte temp/cache (DI kontejner).
  9. Sekci na frontu přidáte presenteru detailu objektu traitem ExpressPaymentTrait: findExpressPaymentTariffs(), getExpressPaymentReference() (id objektu), getExpressPaymentModuleCode(). Vzor je Front\Bazaar\…\AdvertDetailTrait.
  10. Rozšiřte harness tests/Manual/System/payment_gateway_auth_harness.php (sekce 13) o platnou SMS, neaktivní objekt a špatnou cenu.

Postup: přidat službu nad jiným ceníkem#

Když modul rychlé ceny nemá (tarify členství), implementujte rozhraní přímo — vzor MembershipExpressServiceProvider:

  1. Rozhodněte, co je slovo služby. Svažte ho jen s tarify, které callback opravdu umí aktivovat. UserPaymentRepository prodlužuje member_until tarifům s duration > 0 a připisuje kredity tarifům s credits > 0; tarif s oběma nulami by se zaplatil a nic neudělal, proto se nenabízí.
  2. findOffers($paymentGateId) vrací aktivní tarify × aktivní nenulové ceny pro bránu, která SMS přijala. Cena musí být konečná (brána má final_prices).
  3. resolve() ověří objekt čerstvým čtením (UserManager::findFresh() — zablokování musí platit hned, ne až po vypršení cache).
  4. createPaymentItems() založí položku neaktivní; afterSave manageru pak nic neprodlužuje ani nepřipisuje a vše udělá callback jednou.

Idempotence#

Agregátor opakuje tutéž SMS až dvanáctkrát (stejné id, rostoucí att).

  • id agregátora se ukládá do cms_system_payments.gateway_payment_id bez prefixu — doručenka páruje parametr request přímo na tento sloupec.
  • Unikátní index (cms_system_payment_gates_payment_gate_id, gateway_payment_id) brání dvěma platbám ze dvou souběžných pokusů. Prohraný pokus dostane z saveExpressPayment() null a odpoví neutrálně; agregátor se zeptá znovu a dostane uloženou odpověď.
  • Odpověď se ukládá do cms_system_payment_error_logs (status_text = expressReply, text v long_message) a opakování ji vrátí beze změny — u MT čísla i se stejnou hladinou.
  • Callback běží jednou za transakci (claimExecution), stejně jako u formulářové platby — viz opakovaná platba.

Neúspěšná SMS jako reklamace#

Každá chybová větev po ověření původu (nesmyslný text, cizí klíčové slovo, neznámý modul nebo služba, neexistující či neaktivní objekt, neznámé číslo, špatná cena, chybějící callback, selhání položky) založí platbu FAILED s gateway_payment_id:

  • dohledatelnost — u MO čísla operátor peníze strhl při odeslání; číslo platby je kód reklamace v odpovědi a popis platby nese důvod (Expresní SMS neaktivována: priceMismatch),
  • idempotence — opakovaný pokus najde platbu a vrátí tutéž odpověď,
  • bez callbacku a bez položek — nic se neaktivuje.

U MT čísla nese odpověď ;FREE<hladina>, takže se zákazníkovi nic neúčtuje. MobilniplatbyGateway::checkDelivery() odmítne doručenku k platbě, která není PENDING — jinak by DELIVERED k FAILED platbě („FREE“) platbu dodatečně zaplatila.

Odmítnutí kvůli původu (chybějící nebo špatný secret) platbu nezakládá: kdo nezná tajemství, nesmí do tabulky plateb zapsat ani řádek.

Testy#

  • tests/Unit/Core/Payments/ExpressSmsParserTest.php — parser a compose().
  • tests/Manual/System/payment_gateway_auth_harness.php, sekce 13 — celá cesta přes ApiBridge: platná SMS, opakovaná att, špatná cena, neexistující / neaktivní objekt, klíčové slovo, secret, formulářová SMS, doručenka u FAILED; 13.D diskuze; 13.U členství (kredity jednou, MT hladina + doručenka, zablokovaný uživatel). Fixtures po sobě maže. Spouštění: NETTE_DEBUG=1 php -d memory_limit=1G tests/Manual/System/payment_gateway_auth_harness.php.

Nastavení cookies