Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/IvanchoDev89/maleku-system/llms.txt

Use this file to discover all available pages before exploring further.

Maleku System’s payment layer uses Stripe Checkout for customer-facing payment pages and Stripe Connect for automated vendor payouts. When a customer completes checkout, Stripe routes the full amount to the platform account; the platform then transfers the vendor’s share (90%) directly to the vendor’s Connect Express account while retaining a 10% commission. Webhook events drive booking status updates, keeping the database in sync with Stripe’s payment lifecycle without polling.
The following environment variables are required for payments to function:
  • STRIPE_SECRET_KEY — your Stripe secret key (sk_live_... or sk_test_...)
  • STRIPE_PUBLISHABLE_KEY — your Stripe publishable key (pk_live_... or pk_test_...)
  • STRIPE_WEBHOOK_SECRET — the webhook signing secret from the Stripe dashboard or Stripe CLI (whsec_...)
The frontend fetches the publishable key at runtime from GET /api/v1/stripe/config.

Payment Flow

1

Customer Creates a Booking

The customer calls POST /api/v1/bookings/property or POST /api/v1/bookings/tour. A booking record is created in PENDING status with a confirmation_code but no charge is made yet.
2

Checkout Session Created

The frontend calls POST /api/v1/stripe/checkout with the booking_id, a success_url, and a cancel_url. The backend calls create_checkout_session() from stripe_service.py, which builds a Stripe Checkout Session with line items and — when the vendor has a connected Stripe account — configures payment_intent_data.transfer_data to automatically split the payment.
3

Customer Completes Payment on Stripe

The customer is redirected to Stripe’s hosted checkout page (CheckoutResponse.checkout_url). Card details are entered entirely on Stripe’s infrastructure; Maleku System never handles raw card data.
4

Stripe Calls the Webhook

On payment completion, Stripe sends a checkout.session.completed event (or payment_intent.succeeded) to POST /api/v1/stripe/webhook. The signature is validated using STRIPE_WEBHOOK_SECRET before any processing occurs.
5

Booking Status Updated to Confirmed

The webhook handler calls update_booking_payment_status(), which sets booking.status = BookingStatus.CONFIRMED and stores the stripe_payment_intent_id. A payment receipt email is then dispatched to the customer.
6

Vendor Receives Payout Minus Commission

If the vendor has a connected Stripe Express account (vendor.stripe_connected = True), Stripe automatically executes the transfer defined in payment_intent_data.transfer_data, sending the vendor 90% of the booking total. Vendors without a Connect account are flagged as manual_payout: true in the payment intent metadata for manual reconciliation.

Commission Model

The platform charges a 10% commission on every completed booking. The rate is set by STRIPE_COMMISSION_RATE=0.10 in the backend environment and can be overridden per vendor via the commission_rate column on the Vendor model.
# From pricing_service.py
def calculate_commission_breakdown(amount: float, commission_rate: float = 0.10) -> dict:
    commission = round(amount * commission_rate, 2)
    vendor_amount = round(amount - commission, 2)
    return {
        "amount": amount,
        "commission_rate": commission_rate,
        "commission": commission,
        "vendor_amount": vendor_amount,
    }
When building the Checkout Session, the split is computed in cents and passed to Stripe:
vendor_amount  = int((booking.total_amount - booking.commission_amount) * 100)
platform_amount = int(booking.commission_amount * 100)  # application_fee_amount

Webhook Handling

All incoming webhook events are received at POST /api/v1/stripe/webhook. Signature Validation Every request must carry a Stripe-Signature header. The handler calls construct_webhook_event() from stripe_service.py, which wraps stripe.Webhook.construct_event() with the configured STRIPE_WEBHOOK_SECRET. Requests with an invalid or missing signature raise a 400 Bad Request. Idempotency To prevent replay attacks, the event_id from every processed event is stored in the ProcessedWebhook table. If the same event_id arrives again, the handler returns {"status": "already_processed"} without re-executing any business logic. Handled Events
Stripe EventAction
checkout.session.completedExtracts client_reference_id (booking UUID) and payment_intent, calls update_booking_payment_status()CONFIRMED, sends payment receipt email
payment_intent.succeededUpdates booking status to CONFIRMED, stores stripe_payment_intent_id
payment_intent.payment_failedUpdates booking status to CANCELLED, logs the failure reason from last_payment_error
charge.refundedLooks up the booking by stripe_payment_intent_id, sets status to REFUNDED

Refunds

Full or partial refunds are processed by the refund_payment() function in stripe_service.py:
def refund_payment(payment_intent_id: str, amount: float | None = None, reason: str | None = None):
    refund_data = {"payment_intent": payment_intent_id}
    if amount:
        refund_data["amount"] = int(amount * 100)  # Convert to cents
    refund = stripe.Refund.create(**refund_data)
    return {"refund_id": refund.id, "amount": refund.amount / 100, "status": refund.status}
The endpoint POST /api/v1/stripe/bookings/{booking_id}/refund accepts an optional amount (defaults to the full booking total) and an optional reason. Only vendors and admins may issue refunds. After a full refund, the booking status is immediately set to REFUNDED; partial refunds leave the status unchanged.

Local Testing

Use the Stripe CLI to forward webhook events to your local backend during development:
# 1. Log in to the Stripe CLI
stripe login

# 2. Forward events to the local webhook endpoint
stripe listen --forward-to localhost:8000/api/v1/stripe/webhook

# 3. Copy the printed signing secret (whsec_...) into STRIPE_WEBHOOK_SECRET in backend/.env

# 4. Trigger a test checkout completion event
stripe trigger checkout.session.completed
The Stripe CLI prints every forwarded event and its response code, making it easy to debug webhook handler logic without deploying to a public URL.

Vendor Connect Accounts

Vendors must complete Stripe Express onboarding before automatic payouts can be sent to them. Two functions in stripe_service.py manage this lifecycle: create_vendor_connect_account(vendor, refresh_url, return_url) Creates a Stripe Express account for the vendor (country CR, business type individual, card payments and transfers enabled) and returns an onboarding_url that the vendor must visit to submit their KYC information.
account = stripe.Account.create(
    type="express",
    country="CR",
    email=vendor.email,
    business_type="individual",
    capabilities={"card_payments": {"requested": True}, "transfers": {"requested": True}},
)
get_connect_account_status(account_id) Retrieves the account’s charges_enabled, payouts_enabled, details_submitted, and requirements fields. The GET /api/v1/stripe/vendor/connect endpoint calls this to determine if the vendor needs to complete additional verification steps before payouts are enabled. Once charges_enabled and payouts_enabled are both True, the vendor’s stripe_connected flag is set to True in the database and future bookings will route payments automatically.
The success_url and cancel_url fields in POST /api/v1/stripe/checkout are validated against an allowlist before being forwarded to Stripe. Only URLs whose hostname matches SITE_URL, www.{SITE_URL}, app.{SITE_URL}, localhost, or 127.0.0.x are accepted. Any other origin causes a 422 Unprocessable Entity before a Checkout Session is created, preventing open-redirect attacks.

Build docs developers (and LLMs) love