Skip to content

Sales

Needs review

Needs review: Stripe and Przelewy24 checkout not exercised end to end against a live gateway; refunds are only used for trial card checks.

The Sales menu is where you sell access to learning content: you wrap courses, webinars, consultations, stationary events and dictionaries in products, learners buy them through a cart, and the platform records orders and payments. Vouchers give discounts at checkout.

This is the commerce stack inherited from Wellms. The roadmap replaces it with a separate Sylius service (see the last section of this page); until then, everything on this page is what runs.

Who uses it: administrators and sales staff with the cart, payment and voucher permissions.

Screen Route What you do there
Orders /sales/orders List orders with subtotal, tax, total, items, buyer and status; filter by date range, user, product and status.
Payments /sales/payments List payments with amount, currency, status and the paid-for object (payable); filter by date range and status.
Vouchers /sales/vouchers List discount codes.
Voucher form /sales/vouchers/:voucherId/ Create or edit a voucher (new creates one).
Products /sales/products List products.
Product form /sales/products/:id, /sales/products/:id/:tab Create or edit a product, in tabs.
Orders list in the admin panel
Products list in the admin panel
  • Attributes: name, the productables it sells (one or more courses, webinars and so on, each with a quantity), type, purchasable flag, price, old price, tax rate, extra fees, duration, per-user and total limits, teaser URL, language, related products and description. For subscription types also the period and duration, auto-renew (recursive) and an optional trial (period and duration). App Store and Play Store IDs appear when the RevenueCat integration package is installed.
  • Media: poster.
  • Categories: categories and tags.
  • Users attached: who owns the product. You can attach or detach users by hand, which grants or removes access without an order.
  • User submissions: answers to custom fields submitted for the product.
  • Template: trigger a notification template manually for every user of the product.

Product types (ProductType): single, bundle, subscription, subscription-all-in.

A voucher has a name, a code, a type, active flag, an “exclude promotions” flag, an active-from and active-to window, a total usage limit, a per-user limit, minimum and maximum cart value, and an amount. It can be limited to products, categories or users, or exclude products and categories.

Voucher types (CouponTypeEnum):

Type Discount
cart_fixed Fixed amount off the cart
cart_percent Percentage off the cart
product_fixed Fixed amount off the included products
product_percent Percentage off the included products

Learners apply a code to their cart with POST /api/cart/voucher and remove it with DELETE /api/cart/voucher.

The payments package picks a driver per payment. Drivers in api/packages/payments/src/Gateway/Drivers:

Driver Used for Configuration
free Always used when the amount is 0; it refuses non-zero payments. None
stripe Card payments through a Stripe PaymentIntent. Callbacks are accepted only with a valid Stripe-Signature header or after the PaymentIntent is fetched from the Stripe API. PAYMENTS_STRIPE_SECRET_KEY, PAYMENTS_STRIPE_PUBLISHABLE_KEY, PAYMENTS_STRIPE_WEBHOOK_SECRET, PAYMENTS_STRIPE_WEBHOOK_TOLERANCE, PAYMENTS_STRIPE_API_BASE
przelewy24 Redirect payments through Przelewy24 (Polish bank transfers, BLIK, cards), using the vendored przelewy24-php API client. PAYMENTS_PRZELEWY24_LIVE, PAYMENTS_PRZELEWY24_MERCHANT_ID, PAYMENTS_PRZELEWY24_POS_ID, PAYMENTS_PRZELEWY24_API_KEY, PAYMENTS_PRZELEWY24_CRC
revenuecat Mobile in-app purchases. Off by default; a purchase is accepted only when a server-side receipt verifier class is configured. PAYMENTS_REVENUECAT_ENABLED, PAYMENTS_REVENUECAT_RECEIPT_VERIFIER

The default gateway (PAYMENTS_DEFAULT_GATEWAY, default Stripe) and currency (PAYMENTS_DEFAULT_CURRENCY, default USD) apply unless the checkout request names a gateway and currency. GET /api/payments-gateways lists the gateways, whether each is enabled and which parameters the frontend must send (for example return_url and payment_method for Stripe, return_url and email for Przelewy24).

Gateway credentials, enable flags, default gateway and default currency are also administrable settings (ulams_payments.*), so they can be changed per tenant in the admin settings without a redeploy. Secret keys are stored but not exposed publicly. See Settings and Settings reference; environment variables are listed in Environment variables.

Payment statuses: new, redirect, paid, failed, cancelled, refunded.

Stripe confirms payments to POST /api/payments-gateways/webhook/stripe on the API host (one URL per Stripe account). In the Stripe dashboard, add an endpoint with that URL and subscribe it to payment_intent.succeeded, payment_intent.payment_failed and payment_intent.canceled. Copy the endpoint’s signing secret (whsec_...) into PAYMENTS_STRIPE_WEBHOOK_SECRET (or the administrable ulams_payments.drivers.stripe.webhook_secret setting). Without the secret the webhook answers 400 and only the payment_intent returned on the checkout redirect can confirm a payment.

When the card needs 3-D Secure, POST /api/cart/pay returns a redirect_url in data. The legacy learner front sends the browser to that URL; the return goes to the return_url of the request.

  1. The learner adds products to the cart (POST /api/cart/products) or buys a single product directly (POST /api/product/{id}/pay, which answers 403 for a product that is not purchasable or whose limits are reached).
  2. POST /api/cart/pay turns the cart into an order (status PROCESSING, or TRIAL_PROCESSING for a trial) and creates a payment for it. Zero-value orders go through the free driver and are paid immediately. Price, currency, product type and trial settings always come from the product; of the request body only gateway, payment_method, return_url, email and channel reach the payment drivers, and anything else (such as currency, amount or has_trial) is ignored.
  3. For Stripe or Przelewy24, the payment either succeeds at once or returns a redirect URL. The gateway later calls /api/payments-gateways/callback/{payment} (Stripe also has /api/payments-gateways/webhook/stripe). The callback runs in a transaction with the payment row locked; rejected (unverified) callbacks are logged and ignored, and an already settled payment is left untouched, so repeated notifications do not fire events twice.
  4. A successful payment fires PaymentSuccess. The cart’s PaymentSuccessListener marks the order PAID (or TRIAL_PAID) and fires OrderPaid. Marking an already paid order is a no-op.
  5. For each order item the order service fires ProductBought and attaches the product to the buyer (products_users, respecting limit_per_user; subscriptions get an end date). Then each productable in the product is attached: for a course this enrols the learner (with the subscription end date, if any) and fires CourseAssigned and CourseAccessStarted.

Products can sell Course, Webinar, Consultation, StationaryEvent and Dictionary (registered in api/app/Providers/ShopServiceProvider.php).

Access can also be granted without payment: attach a user on the product’s Users attached tab, or assign the course directly (see Users).

The cart package schedules:

  • RenewRecursiveProduct hourly and ExpireRecursiveProduct daily, for subscription products.
  • cart:abandoned-event daily at 01:00: finds carts abandoned 24 to 48 hours ago and fires AbandonedCartEvent.

Learners cancel a subscription with POST /api/products/cancel/{id}. See Scheduled jobs.

Area Endpoints
Orders (admin) GET /api/admin/orders, GET /api/admin/orders/{id}, GET /api/admin/orders/export
Products (admin) GET, POST /api/admin/products; GET, PUT, DELETE /api/admin/products/{id}; POST /api/admin/products/{id}/attach and /detach; POST /api/admin/products/{id}/trigger-event-manually/{idTemplate}; /api/admin/productables (list, registered types, product of a productable, attach, detach)
Payments (admin) GET /api/admin/payments, GET /api/admin/payments/{payment}, GET /api/admin/payments/export
Vouchers (admin) GET, POST /api/admin/vouchers; GET, PUT, PATCH, DELETE /api/admin/vouchers/{id}
Learner /api/cart (view, set quantity, add missing, remove, add productable), /api/cart/voucher, /api/cart/pay, /api/product/{id}/pay, /api/products (catalogue, my, cancel), /api/orders, /api/payments, /api/order-invoices/{id}
Gateways GET /api/payments-gateways, callbacks, Stripe webhook

The export endpoints exist in the API; the admin screens do not show an export button. Full list: API endpoints.

Permission Allows
cart_order_list Orders screen and any order
orders_export Order export
products_list Open a product
products_manage Products list, create, edit, delete, attach users
products_list_purchasable, products_buy Learner catalogue and purchase
payment_list, payment_read, payment_export Payments screen, details, export
coupon_list, coupon_read, coupon_create, coupon_update, coupon_delete Vouchers
coupon_use Apply a voucher at checkout

The Sales menu appears when the user has any of cart_order_list, payment_list, coupon_list or products_manage. See Permissions reference and Roles and permissions.

Event Package Used by
OrderCreated cart Email template; MailerLite
OrderPaid, OrderCancelled cart
ProductBought cart MailerLite
ProductAttached, ProductDetached, ProductableAttached, ProductableDetached cart ProductAttached email template
ProductAddedToCart, ProductRemovedFromCart, AbandonedCartEvent cart AbandonedCartEvent: MailerLite
PaymentRegistered, PaymentSuccess, PaymentFailed, PaymentCancelled payments Email templates

Configure the templates under Templates. Full list: Events and notifications.

Key Meaning
ulams_payments.default_gateway Free, Stripe or Przelewy24
ulams_payments.default_currency Default currency
ulams_payments.drivers.* Enable flags and credentials per gateway
ulams_cart.min_product_price Minimum product price (default 0)
invoices.* Payment term (date.pay_until_days), currency, seller name, address, code, VAT, phone, SWIFT
  • Refunds are not an admin action. The only refund path is automatic: a trial paid with a card check is refunded right after the gateway confirms it.
  • GET /api/order-invoices/{id} sits outside the auth:api group; it relies on the order policy and is useful only to an authenticated owner or admin.
  • Access is granted inside the platform from the verified gateway callback, not from the frontend redirect, but there is no reconciliation job for missed callbacks.

Planned: Sylius commerce Coming

Section titled “Planned: Sylius commerce ”

The spec moves commerce to a separate Sylius 2.x headless service (roadmap Phase 6.4): Sylius will own catalogue, cart, checkout, taxes and invoices, while the LMS keeps an entitlements model and stays the only system that decides who has access. Access will be granted only from signed, idempotent Sylius order events plus a reconciliation job, behind a CommerceProvider interface. None of this is implemented yet; see the Roadmap and the Phase 0 commerce audit.