Payments
Payments
Shared across every PremiumPress product. 6 classes in inc/Payments.
CheckoutFlow CheckoutFlow.php
PPT — front-end checkout flow for paid listing plans. Ties together the pieces that already existed separately (Pricing Plans, the Gateways registry, the ppt_orders store, the /callback/ virtual page) into a working purchase loop: 1. A member submits a listing on a PAID plan → held as a draft and sent to the /checkout/ page (see ListingEditor::handleSave). 2. /checkout/ shows the order summary (plan price + tax) and the enabled gateways. "Pay" POSTs to admin-post (handlePay). 3. handlePay creates a pending ppt_orders order and hands off to the chosen gateway via the ppt_gateway_start filter, which returns where to send the buyer. The bundled Test gateway returns the /callback/ URL directly. 4. /callback/ verifies the order token, marks the order complete, activates the plan on the listing (publish + expiry), emails a receipt, and confirms. Real gateway plugins hook ppt_gateway_start to redirect to their hosted payment, then return the buyer to the same /callback/ URL. The /checkout/ and /callback/ pages render through Ppt\Frontend\Pages (which delegates those two slots here), so they sit inside the normal theme chrome.
CheckoutStyles CheckoutStyles.php
The shared checkout stylesheet — the .ppt-sco-* screen used by every user-scoped checkout (credit packs, memberships). It lived inside Ppt\Credits\Checkout, which was fine while credits were the only thing a member bought for themselves. Membership checkout is the second, and the choice was to copy forty lines of CSS or to move them somewhere neither class owns. This is that somewhere; Credits\Checkout::styles() now delegates here, so the two screens cannot drift apart. Prints ONCE per request however many callers ask, so a page that somehow rendered both screens still emits one <style>.
Coupon Coupon.php
Shared coupon codes — validates a code against the admin-configured list (Checkout ▸ Coupons, Ppt\Admin\Checkout::get('coupons', …)) and computes the discount it's worth against a subtotal. Theme-agnostic (Ppt\Payments), so any checkout can honour the one coupon list: the shared credit-pack checkout uses it now, and the _sp Shop cart keeps its own identical _sp\Coupon for its cart flow. Redemption limits: the admin "Limit" field caps how many orders may use a code. Uses are counted in the ppt_coupon_uses option by Payments\CouponRedemptions when an order carrying the code completes (whichever checkout placed it), and find() refuses a code that has reached its limit.
CouponRedemptions CouponRedemptions.php
Coupon redemption counting — the half of "Limit" that was never built. Checkout ▸ Coupons lets the admin cap how many times a code may be used, and Payments\Coupon stored that cap, but nothing ever counted a use, so every limited coupon was effectively unlimited. Rather than teach thirteen checkouts to call Coupon::redeem(), this watches the one thing they all do on success — write order_status = complete on a ppt_orders post — reads the code the order carries through Pricing::orderBreakdown() (whichever meta namespace the checkout used) and counts it once per order.
Pricing Pricing.php
Shared order pricing — coupon + tax, in ONE place. Why this exists: Ppt\Payments\Coupon was already a shared engine, but using it was re-implemented per checkout — its own M_COUPON/M_DISCOUNT consts, its own coupon() and totals() wrappers, its own meta writes and its own form markup, roughly eighty lines copied into each class. The result was that 5 of the 13 checkouts honoured coupon codes and 8 did not, including Payments\CheckoutFlow — the main one — so an admin could switch coupons on and see no field at all on the listing/pricing-plan checkout. Every checkout should call these instead of growing its own copy: $coupon = Pricing::coupon($code, $price); // null when invalid/off/expired $totals = Pricing::totals($price, $coupon['discount'] ?? 0.0); Pricing::writeOrder($orderId, self::META_PREFIX, $totals, $coupon); echo Pricing::couponField($code, $formAction); // '' when coupons are off The per-checkout meta keys stay distinct (each checkout passes its own prefix) so nothing about existing orders changes; only the logic is shared. Tax still comes from CheckoutFlow::totals() — the single tax implementation — applied to the DISCOUNTED subtotal, which is the order every existing coupon-aware checkout uses.
TestGateway TestGateway.php
PPT — bundled Test payment gateway. Unlike real gateways (which ship as separate plugins, e.g. ppt-paypal), this one lives in the theme so there's always a way to exercise the checkout flow end to end without wiring up real credentials. It registers like any other gateway (ppt_payment_gateways), so it appears under Checkout ▸ Gateways with an on/off toggle. When a buyer checks out with it, it bypasses real payment and sends them straight to the /callback/ confirmation step — as if the payment had returned. The admin can set the simulated result (Approved / Declined) to test both the success and failure paths. Disable it before going live.