Skip to content
Open
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
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,64 @@ website_route_rules = [
```

### Payment Gateway Integration
The app supports various payment gateways. For Tabby BNPL integration, check out our companion app: [tabby_frappe](https://github.com/cinnamonlabs/tabby_frappe)

LS Shop supports multiple payment gateways out of the box:

| Gateway | Type | Description |
|---------|------|-------------|
| **Telr** | Credit/Debit Card | Iframe-based checkout via Telr payment gateway |
| **Tabby** | BNPL (Buy Now Pay Later) | Full redirect checkout. Requires [tabby_frappe](https://github.com/cinnamonlabs/tabby_frappe) |
| **Stripe** | Credit/Debit Card | Stripe Checkout Sessions with full redirect |
| **COD** | Cash on Delivery | No online payment required |

#### Stripe Setup

After installing ls_shop, follow these steps to enable Stripe:

1. **Run migrations** to create the Stripe DocTypes:
```bash
bench --site your-site-name migrate
```

2. **Create Mode of Payment** record:
- Go to **Setup > Mode of Payment > + New**
- Name: Stripe, Type: Bank
- Optionally link a default account under the Accounts table

3. **Add Stripe to Sales Order payment mode options**:
- Go to **Customize Form > Sales Order**
- Find the custom_ecommerce_payment_mode field
- Add Stripe to the Options list (one per line)

4. **Configure Stripe API keys**:
- Go to **Stripe Settings** (search in the desk)
- Enter your **Publishable Key** (pk_test_... or pk_live_...)
- Enter your **Secret Key** (sk_test_... or sk_live_...)
- Set **Currency** (default: SAR)
- Enable **Test Mode** if using test keys

5. **Enable Stripe in Lifestyle Settings**:
- Go to **Lifestyle Settings**
- Check **Stripe Enabled**

#### Stripe Test Cards

When using Stripe in test mode, use these [test card numbers](https://docs.stripe.com/testing#cards):

| Card Number | Description |
|-------------|-------------|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 3220 | 3D Secure authentication required |
| 4000 0000 0000 0002 | Declined |

Use any future expiry date, any 3-digit CVC, and any postal code.

#### Stripe Features

- **Checkout Sessions**: Secure, Stripe-hosted payment page
- **Automatic status sync**: Payment status is synced from Stripe on confirmation
- **Refunds**: Automatic refund processing when a return Payment Entry (type: Pay) is submitted with Mode of Payment = Stripe
- **Multi-currency**: Supports any currency configured in Stripe


## 🏢 About BWH Studios
Expand Down
50 changes: 45 additions & 5 deletions ls_shop/api/payments.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from enum import StrEnum
try:
from enum import StrEnum
except ImportError:
from enum import Enum
class StrEnum(str, Enum):
pass

import frappe
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
Expand All @@ -17,6 +22,7 @@ class PaymentMode(StrEnum):
TELR = "telr"
TABBY = "tabby"
COD = "cod"
STRIPE = "stripe"


@frappe.whitelist()
Expand Down Expand Up @@ -73,6 +79,24 @@ def initiate_checkout_with_mode(payment_mode: PaymentMode):
}
).insert(ignore_permissions=True)

# Get a valid email for Stripe (contact email may not be valid)
stripe_customer_email = customer_contact.email_id
if not stripe_customer_email or "@" not in str(stripe_customer_email):
stripe_customer_email = frappe.db.get_value("User", frappe.session.user, "email") or frappe.session.user

if payment_mode == PaymentMode.STRIPE:
payment_request = frappe.get_doc(
{
"doctype": "Stripe Payment Request",
"amount": quotation.rounded_total,
"currency_code": "SAR" if frappe.conf.developer_mode else quotation.currency,
"ref_doctype": quotation.doctype,
"ref_docname": quotation.name,
"customer_email": stripe_customer_email,
"customer_name": quotation.customer_name,
}
).insert(ignore_permissions=True)

return {"payment_request": payment_request}


Expand Down Expand Up @@ -227,6 +251,17 @@ def confirm_payment(payment_mode: PaymentMode, reference_id: str):
return payment_request


if payment_mode == PaymentMode.STRIPE:
payment_request = frappe.get_doc("Stripe Payment Request", {"stripe_session_id": reference_id})
payment_request.sync_status()

if payment_request.status == "Paid":
quote_name = payment_request.ref_docname
submit_quotation_and_create_order(quote_name, payment_mode, payment_request.stripe_session_id)

return payment_request


def submit_quotation_and_create_order(
quote_name: str, payment_mode: PaymentMode, payment_reference: str = ""
):
Expand All @@ -246,10 +281,15 @@ def submit_quotation_and_create_order(

if payment_mode != payment_mode.COD:
so.submit()
payment_request_doctype = (
"Telr Payment Request" if payment_mode == PaymentMode.TELR else "Tabby Payment Request"
)
payment_order_ref_field = "telr_order_ref" if payment_mode == PaymentMode.TELR else "tabby_order_ref"

# Map payment mode to doctype and ref field
payment_doctype_map = {
PaymentMode.TELR: ("Telr Payment Request", "telr_order_ref"),
PaymentMode.TABBY: ("Tabby Payment Request", "tabby_order_ref"),
PaymentMode.STRIPE: ("Stripe Payment Request", "stripe_session_id"),
}

payment_request_doctype, payment_order_ref_field = payment_doctype_map[payment_mode]
payment_request = frappe.get_doc(
payment_request_doctype, {payment_order_ref_field: payment_reference}
)
Expand Down
1 change: 1 addition & 0 deletions ls_shop/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"Payment Entry": {
"on_submit": [
"ls_shop.lifestyle_shop_ecommerce.doctype.telr_payment_request.telr_payment_request.refund_payment_for_payment_entry",
"ls_shop.lifestyle_shop_ecommerce.doctype.stripe_payment_request.stripe_payment_request.refund_payment_for_payment_entry",
],
},
"Sales Order": {
Expand Down
Loading