From 124f27810ec263fde7c9714968d0e3e4634e5a9d Mon Sep 17 00:00:00 2001 From: work-xcode-ae Date: Tue, 17 Feb 2026 11:59:38 +0400 Subject: [PATCH] Add Stripe Payment Gateway & Fix Mobile/UI Issues 1- Added Stripe payment gateway. 2- Fixed slider issue: The section is now hidden if there are no images, instead of displaying a blank white page. 3- Fixed login icon menu: Corrected the layout when the user accesses the site from the mobile application. --- README.md | 59 +- ls_shop/api/payments.py | 50 +- ls_shop/hooks.py | 1 + .../lifestyle_settings.json | 1403 +++++++++-------- .../stripe_payment_request/__init__.py | 0 .../stripe_payment_request.json | 178 +++ .../stripe_payment_request.py | 92 ++ .../doctype/stripe_settings/__init__.py | 0 .../stripe_settings/stripe_settings.json | 98 ++ .../stripe_settings/stripe_settings.py | 101 ++ .../templates/components/user_dropdown.html | 2 +- ls_shop/templates/macros/dropdown.html | 5 +- ls_shop/www/account/orders/confirmation.html | 19 +- ls_shop/www/cart/checkout.html | 19 + ls_shop/www/cart/checkout.py | 1 + ls_shop/www/index.html | 67 +- 16 files changed, 1369 insertions(+), 726 deletions(-) create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/__init__.py create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.json create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.py create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/__init__.py create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.json create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.py diff --git a/README.md b/README.md index a0bb732..846f383 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ls_shop/api/payments.py b/ls_shop/api/payments.py index 6e5e5a9..c8ac6a3 100644 --- a/ls_shop/api/payments.py +++ b/ls_shop/api/payments.py @@ -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 @@ -17,6 +22,7 @@ class PaymentMode(StrEnum): TELR = "telr" TABBY = "tabby" COD = "cod" + STRIPE = "stripe" @frappe.whitelist() @@ -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} @@ -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 = "" ): @@ -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} ) diff --git a/ls_shop/hooks.py b/ls_shop/hooks.py index fe8a872..19ec7bf 100644 --- a/ls_shop/hooks.py +++ b/ls_shop/hooks.py @@ -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": { diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json index bc22fd5..2b505c4 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json @@ -1,699 +1,706 @@ { - "actions": [], - "allow_rename": 1, - "creation": "2025-03-29 16:14:22.177674", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "branding_section", - "store_name", - "column_break_brand", - "brand_logo", - "footer_logo", - "column_break_favicon", - "favicon", - "cod_section", - "cod_charge_applicable_below", - "charge_account_head", - "column_break_nnvn", - "cod_charge", - "ecommerce_warehouse", - "price_list_section", - "default_price_list", - "column_break_sgnu", - "sale_price_list", - "delivery_section", - "shipping_rule", - "reason_for_return", - "column_break_pkyw", - "return_period", - "print_format", - "payment_mode_section", - "telr_enabled", - "column_break_iirq", - "tabby_enabled", - "column_break_oopm", - "cod_enabled", - "bulk_import_tab", - "create_variants_automatically_on_configurator_creation", - "section_break_ekez", - "based_on_attribute", - "attribute_name_field", - "publish_variants_for_all_templates", - "view_logs", - "demo_data_section", - "install_demo_data", - "column_break_demo", - "publish_all_items", - "item_group_section", - "ecommerce_item_group_mapping", - "sync_item_group_mapping_to_ecommerce_items", - "contact_information_tab", - "contact_phone", - "column_break_contact", - "contact_email", - "working_hours", - "social_media_tab", - "facebook_url", - "twitter_url", - "column_break_social", - "instagram_url", - "snapchat_url", - "column_break_social2", - "tiktok_url", - "footer_customization_tab", - "newsletter_title", - "column_break_newsletter", - "newsletter_description", - "section_break_footer_assets", - "payment_methods_image", - "column_break_footer_assets", - "vat_certificate_image", - "section_break_copyright", - "copyright_text", - "section_break_footer_sections", - "footer_sections", - "color_scheme_tab", - "primary_color", - "primary_hover_color", - "column_break_colors1", - "link_color", - "link_hover_color", - "column_break_colors2", - "accent_color", - "border_accent_color", - "section_break_ui_colors", - "button_bg_color", - "badge_bg_color", - "strikethrough_color", - "column_break_ui_colors1", - "heading_accent_color", - "brand_text_color", - "column_break_ui_colors2", - "secondary_accent_color", - "form_accent_color", - "focus_ring_color", - "section_break_footer_colors", - "footer_bg_color", - "column_break_footer_colors", - "footer_text_color", - "communication_tab", - "order_section", - "order_confirmation_email_template", - "item_in_stock_email_template", - "column_break_tald", - "order_cancellation_email_template", - "logo_url", - "email_section", - "cc_email" - ], - "fields": [ - { - "fieldname": "branding_section", - "fieldtype": "Section Break", - "label": "Branding" - }, - { - "description": "Name displayed in browser tab and site title", - "fieldname": "store_name", - "fieldtype": "Data", - "label": "Store Name" - }, - { - "fieldname": "column_break_brand", - "fieldtype": "Column Break" - }, - { - "description": "Logo displayed in header navigation", - "fieldname": "brand_logo", - "fieldtype": "Attach Image", - "label": "Brand Logo" - }, - { - "description": "Logo displayed in footer", - "fieldname": "footer_logo", - "fieldtype": "Attach Image", - "label": "Footer Logo" - }, - { - "fieldname": "column_break_favicon", - "fieldtype": "Column Break" - }, - { - "description": "Icon displayed in browser tab (16x16 or 32x32 px)", - "fieldname": "favicon", - "fieldtype": "Attach", - "label": "Favicon" - }, - { - "fieldname": "delivery_section", - "fieldtype": "Section Break", - "label": "Shipping & Returns" - }, - { - "fieldname": "shipping_rule", - "fieldtype": "Link", - "label": "Shipping Rule", - "link_filters": "[[\"Shipping Rule\",\"calculate_based_on\",\"=\",\"Net Total\"],[\"Shipping Rule\",\"disabled\",\"=\",0]]", - "options": "Shipping Rule" - }, - { - "fieldname": "cod_section", - "fieldtype": "Section Break", - "label": "Cash on Delivery" - }, - { - "fieldname": "column_break_nnvn", - "fieldtype": "Column Break" - }, - { - "fieldname": "cod_charge", - "fieldtype": "Currency", - "label": "COD Charge" - }, - { - "fieldname": "cod_charge_applicable_below", - "fieldtype": "Currency", - "label": "COD Charge Applicable Below" - }, - { - "fieldname": "charge_account_head", - "fieldtype": "Link", - "label": "Charge Account Head", - "options": "Account" - }, - { - "fieldname": "ecommerce_warehouse", - "fieldtype": "Link", - "label": "Ecommerce Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "column_break_pkyw", - "fieldtype": "Column Break" - }, - { - "fieldname": "return_period", - "fieldtype": "Int", - "hide_seconds": 1, - "label": "Return Period (Days) After Invoice Generated", - "non_negative": 1 - }, - { - "fieldname": "price_list_section", - "fieldtype": "Section Break", - "label": "Price List" - }, - { - "fieldname": "default_price_list", - "fieldtype": "Link", - "label": "Default Price List", - "options": "Price List" - }, - { - "fieldname": "column_break_sgnu", - "fieldtype": "Column Break" - }, - { - "fieldname": "sale_price_list", - "fieldtype": "Link", - "label": "Sale Price List", - "options": "Price List" - }, - { - "fieldname": "reason_for_return", - "fieldtype": "Table", - "label": "Reason for Return", - "options": "Return Reason" - }, - { - "fieldname": "print_format", - "fieldtype": "Link", - "label": "Print Format", - "link_filters": "[[\"Print Format\",\"doc_type\",\"=\",\"Sales Invoice\"]]", - "options": "Print Format" - }, - { - "fieldname": "bulk_import_tab", - "fieldtype": "Tab Break", - "label": "Bulk Actions / Import" - }, - { - "default": "0", - "fieldname": "create_variants_automatically_on_configurator_creation", - "fieldtype": "Check", - "label": "Create variants automatically on Configurator Creation" - }, - { - "fieldname": "publish_variants_for_all_templates", - "fieldtype": "Button", - "label": "Publish Variants for All Templates" - }, - { - "fieldname": "section_break_ekez", - "fieldtype": "Section Break" - }, - { - "fieldname": "based_on_attribute", - "fieldtype": "Link", - "label": "Based on Attribute", - "options": "Item Attribute" - }, - { - "fieldname": "communication_tab", - "fieldtype": "Tab Break", - "label": "Communication" - }, - { - "fieldname": "order_section", - "fieldtype": "Section Break", - "label": "Order" - }, - { - "fieldname": "order_confirmation_email_template", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Order Confirmation Email Template", - "options": "Email Template", - "reqd": 1 - }, - { - "fieldname": "column_break_tald", - "fieldtype": "Column Break" - }, - { - "fieldname": "order_cancellation_email_template", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Order Cancellation Email Template", - "options": "Email Template", - "reqd": 1 - }, - { - "fieldname": "logo_url", - "fieldtype": "Data", - "label": "Logo URL", - "options": "URL" - }, - { - "fieldname": "item_in_stock_email_template", - "fieldtype": "Link", - "label": "Item In Stock Email Template", - "options": "Email Template", - "reqd": 1 - }, - { - "fieldname": "item_group_section", - "fieldtype": "Section Break", - "label": "Item Group" - }, - { - "fieldname": "contact_information_tab", - "fieldtype": "Tab Break", - "label": "Contact Information" - }, - { - "fieldname": "contact_phone", - "fieldtype": "Data", - "label": "Contact Phone" - }, - { - "fieldname": "column_break_contact", - "fieldtype": "Column Break" - }, - { - "fieldname": "contact_email", - "fieldtype": "Data", - "label": "Contact Email", - "options": "Email" - }, - { - "fieldname": "working_hours", - "fieldtype": "Data", - "label": "Working Hours" - }, - { - "fieldname": "social_media_tab", - "fieldtype": "Tab Break", - "label": "Social Media" - }, - { - "fieldname": "facebook_url", - "fieldtype": "Data", - "label": "Facebook URL", - "options": "URL" - }, - { - "fieldname": "twitter_url", - "fieldtype": "Data", - "label": "Twitter/X URL", - "options": "URL" - }, - { - "fieldname": "column_break_social", - "fieldtype": "Column Break" - }, - { - "fieldname": "instagram_url", - "fieldtype": "Data", - "label": "Instagram URL", - "options": "URL" - }, - { - "fieldname": "snapchat_url", - "fieldtype": "Data", - "label": "Snapchat URL", - "options": "URL" - }, - { - "fieldname": "column_break_social2", - "fieldtype": "Column Break" - }, - { - "fieldname": "tiktok_url", - "fieldtype": "Data", - "label": "TikTok URL", - "options": "URL" - }, - { - "fieldname": "footer_customization_tab", - "fieldtype": "Tab Break", - "label": "Footer Customization" - }, - { - "fieldname": "newsletter_title", - "fieldtype": "Data", - "label": "Newsletter Title" - }, - { - "fieldname": "column_break_newsletter", - "fieldtype": "Column Break" - }, - { - "fieldname": "newsletter_description", - "fieldtype": "Text", - "label": "Newsletter Description" - }, - { - "fieldname": "section_break_footer_assets", - "fieldtype": "Section Break", - "label": "Footer Assets" - }, - { - "fieldname": "payment_methods_image", - "fieldtype": "Attach Image", - "label": "Payment Methods Image" - }, - { - "fieldname": "column_break_footer_assets", - "fieldtype": "Column Break" - }, - { - "fieldname": "vat_certificate_image", - "fieldtype": "Attach Image", - "label": "VAT Certificate Image" - }, - { - "fieldname": "section_break_copyright", - "fieldtype": "Section Break", - "label": "Copyright & Legal" - }, - { - "fieldname": "copyright_text", - "fieldtype": "Data", - "label": "Copyright Text" - }, - { - "fieldname": "section_break_footer_sections", - "fieldtype": "Section Break", - "label": "Footer Sections" - }, - { - "fieldname": "footer_sections", - "fieldtype": "Table", - "label": "Footer Sections", - "options": "Footer Section Mapping" - }, - { - "fieldname": "color_scheme_tab", - "fieldtype": "Tab Break", - "label": "Color Scheme" - }, - { - "default": "#b91c1c", - "fieldname": "primary_color", - "fieldtype": "Color", - "label": "Primary Color" - }, - { - "default": "#991b1b", - "fieldname": "primary_hover_color", - "fieldtype": "Color", - "label": "Primary Hover Color" - }, - { - "fieldname": "column_break_colors1", - "fieldtype": "Column Break" - }, - { - "default": "#7f1d1d", - "fieldname": "link_color", - "fieldtype": "Color", - "label": "Link Color" - }, - { - "default": "#991b1b", - "fieldname": "link_hover_color", - "fieldtype": "Color", - "label": "Link Hover Color" - }, - { - "fieldname": "column_break_colors2", - "fieldtype": "Column Break" - }, - { - "default": "#b91c1c", - "fieldname": "accent_color", - "fieldtype": "Color", - "label": "Accent Color" - }, - { - "default": "#b91c1c", - "fieldname": "border_accent_color", - "fieldtype": "Color", - "label": "Border Accent Color" - }, - { - "fieldname": "section_break_ui_colors", - "fieldtype": "Section Break", - "label": "UI Element Colors" - }, - { - "default": "#b91c1c", - "fieldname": "button_bg_color", - "fieldtype": "Color", - "label": "Button Background Color" - }, - { - "fieldname": "column_break_ui_colors1", - "fieldtype": "Column Break" - }, - { - "default": "#b91c1c", - "fieldname": "strikethrough_color", - "fieldtype": "Color", - "label": "Strikethrough Text Color" - }, - { - "default": "#b91c1c", - "fieldname": "badge_bg_color", - "fieldtype": "Color", - "label": "Badge Background Color" - }, - { - "fieldname": "column_break_ui_colors2", - "fieldtype": "Column Break" - }, - { - "default": "#991b1b", - "fieldname": "heading_accent_color", - "fieldtype": "Color", - "label": "Heading Accent Color" - }, - { - "default": "#b91c1c", - "fieldname": "brand_text_color", - "fieldtype": "Color", - "label": "Brand Text Color" - }, - { - "default": "#991b1b", - "fieldname": "secondary_accent_color", - "fieldtype": "Color", - "label": "Secondary Accent Color" - }, - { - "default": "#b91c1c", - "fieldname": "form_accent_color", - "fieldtype": "Color", - "label": "Form Accent Color" - }, - { - "default": "#b91c1c", - "fieldname": "focus_ring_color", - "fieldtype": "Color", - "label": "Focus Ring Color" - }, - { - "fieldname": "section_break_footer_colors", - "fieldtype": "Section Break", - "label": "Footer Colors" - }, - { - "default": "#111827", - "fieldname": "footer_bg_color", - "fieldtype": "Color", - "label": "Footer Background Color" - }, - { - "fieldname": "column_break_footer_colors", - "fieldtype": "Column Break" - }, - { - "default": "#ffffff", - "fieldname": "footer_text_color", - "fieldtype": "Color", - "label": "Footer Text Color" - }, - { - "fieldname": "ecommerce_item_group_mapping", - "fieldtype": "Table", - "label": "eCommerce Item Group Mapping", - "options": "Item Group Map" - }, - { - "fieldname": "sync_item_group_mapping_to_ecommerce_items", - "fieldtype": "Button", - "label": "Sync Item Group Mapping to Existing Items" - }, - { - "fieldname": "payment_mode_section", - "fieldtype": "Section Break", - "label": "Payment Mode" - }, - { - "fieldname": "column_break_iirq", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_oopm", - "fieldtype": "Column Break" - }, - { - "default": "1", - "fieldname": "telr_enabled", - "fieldtype": "Check", - "label": "Telr" - }, - { - "default": "1", - "fieldname": "tabby_enabled", - "fieldtype": "Check", - "label": "Tabby" - }, - { - "default": "1", - "fieldname": "cod_enabled", - "fieldtype": "Check", - "label": "COD" - }, - { - "fieldname": "email_section", - "fieldtype": "Section Break", - "label": "Email" - }, - { - "fieldname": "cc_email", - "fieldtype": "Data", - "label": "CC Email", - "options": "Email" - }, - { - "fieldname": "attribute_name_field", - "fieldtype": "Data", - "label": "Attribute Name Field" - }, - { - "fieldname": "view_logs", - "fieldtype": "Button", - "label": "View Logs" - }, - { - "fieldname": "demo_data_section", - "fieldtype": "Section Break", - "label": "Demo Data & Testing" - }, - { - "fieldname": "install_demo_data", - "fieldtype": "Button", - "label": "Install Demo Data" - }, - { - "fieldname": "column_break_demo", - "fieldtype": "Column Break" - }, - { - "fieldname": "publish_all_items", - "fieldtype": "Button", - "label": "Publish All Items to Website" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2025-10-07 15:34:41.977684", - "modified_by": "Administrator", - "module": "Lifestyle Shop Ecommerce", - "name": "Lifestyle Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Website Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Administrator", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "All" - } - ], - "row_format": "Dynamic", - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} + "actions": [], + "allow_rename": 1, + "creation": "2025-03-29 16:14:22.177674", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "branding_section", + "store_name", + "column_break_brand", + "brand_logo", + "footer_logo", + "column_break_favicon", + "favicon", + "cod_section", + "cod_charge_applicable_below", + "charge_account_head", + "column_break_nnvn", + "cod_charge", + "ecommerce_warehouse", + "price_list_section", + "default_price_list", + "column_break_sgnu", + "sale_price_list", + "delivery_section", + "shipping_rule", + "reason_for_return", + "column_break_pkyw", + "return_period", + "print_format", + "payment_mode_section", + "telr_enabled", + "column_break_iirq", + "tabby_enabled", + "column_break_oopm", + "cod_enabled", + "stripe_enabled", + "bulk_import_tab", + "create_variants_automatically_on_configurator_creation", + "section_break_ekez", + "based_on_attribute", + "attribute_name_field", + "publish_variants_for_all_templates", + "view_logs", + "demo_data_section", + "install_demo_data", + "column_break_demo", + "publish_all_items", + "item_group_section", + "ecommerce_item_group_mapping", + "sync_item_group_mapping_to_ecommerce_items", + "contact_information_tab", + "contact_phone", + "column_break_contact", + "contact_email", + "working_hours", + "social_media_tab", + "facebook_url", + "twitter_url", + "column_break_social", + "instagram_url", + "snapchat_url", + "column_break_social2", + "tiktok_url", + "footer_customization_tab", + "newsletter_title", + "column_break_newsletter", + "newsletter_description", + "section_break_footer_assets", + "payment_methods_image", + "column_break_footer_assets", + "vat_certificate_image", + "section_break_copyright", + "copyright_text", + "section_break_footer_sections", + "footer_sections", + "color_scheme_tab", + "primary_color", + "primary_hover_color", + "column_break_colors1", + "link_color", + "link_hover_color", + "column_break_colors2", + "accent_color", + "border_accent_color", + "section_break_ui_colors", + "button_bg_color", + "badge_bg_color", + "strikethrough_color", + "column_break_ui_colors1", + "heading_accent_color", + "brand_text_color", + "column_break_ui_colors2", + "secondary_accent_color", + "form_accent_color", + "focus_ring_color", + "section_break_footer_colors", + "footer_bg_color", + "column_break_footer_colors", + "footer_text_color", + "communication_tab", + "order_section", + "order_confirmation_email_template", + "item_in_stock_email_template", + "column_break_tald", + "order_cancellation_email_template", + "logo_url", + "email_section", + "cc_email" + ], + "fields": [ + { + "fieldname": "branding_section", + "fieldtype": "Section Break", + "label": "Branding" + }, + { + "description": "Name displayed in browser tab and site title", + "fieldname": "store_name", + "fieldtype": "Data", + "label": "Store Name" + }, + { + "fieldname": "column_break_brand", + "fieldtype": "Column Break" + }, + { + "description": "Logo displayed in header navigation", + "fieldname": "brand_logo", + "fieldtype": "Attach Image", + "label": "Brand Logo" + }, + { + "description": "Logo displayed in footer", + "fieldname": "footer_logo", + "fieldtype": "Attach Image", + "label": "Footer Logo" + }, + { + "fieldname": "column_break_favicon", + "fieldtype": "Column Break" + }, + { + "description": "Icon displayed in browser tab (16x16 or 32x32 px)", + "fieldname": "favicon", + "fieldtype": "Attach", + "label": "Favicon" + }, + { + "fieldname": "delivery_section", + "fieldtype": "Section Break", + "label": "Shipping & Returns" + }, + { + "fieldname": "shipping_rule", + "fieldtype": "Link", + "label": "Shipping Rule", + "link_filters": "[[\"Shipping Rule\",\"calculate_based_on\",\"=\",\"Net Total\"],[\"Shipping Rule\",\"disabled\",\"=\",0]]", + "options": "Shipping Rule" + }, + { + "fieldname": "cod_section", + "fieldtype": "Section Break", + "label": "Cash on Delivery" + }, + { + "fieldname": "column_break_nnvn", + "fieldtype": "Column Break" + }, + { + "fieldname": "cod_charge", + "fieldtype": "Currency", + "label": "COD Charge" + }, + { + "fieldname": "cod_charge_applicable_below", + "fieldtype": "Currency", + "label": "COD Charge Applicable Below" + }, + { + "fieldname": "charge_account_head", + "fieldtype": "Link", + "label": "Charge Account Head", + "options": "Account" + }, + { + "fieldname": "ecommerce_warehouse", + "fieldtype": "Link", + "label": "Ecommerce Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "column_break_pkyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "return_period", + "fieldtype": "Int", + "hide_seconds": 1, + "label": "Return Period (Days) After Invoice Generated", + "non_negative": 1 + }, + { + "fieldname": "price_list_section", + "fieldtype": "Section Break", + "label": "Price List" + }, + { + "fieldname": "default_price_list", + "fieldtype": "Link", + "label": "Default Price List", + "options": "Price List" + }, + { + "fieldname": "column_break_sgnu", + "fieldtype": "Column Break" + }, + { + "fieldname": "sale_price_list", + "fieldtype": "Link", + "label": "Sale Price List", + "options": "Price List" + }, + { + "fieldname": "reason_for_return", + "fieldtype": "Table", + "label": "Reason for Return", + "options": "Return Reason" + }, + { + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "link_filters": "[[\"Print Format\",\"doc_type\",\"=\",\"Sales Invoice\"]]", + "options": "Print Format" + }, + { + "fieldname": "bulk_import_tab", + "fieldtype": "Tab Break", + "label": "Bulk Actions / Import" + }, + { + "default": "0", + "fieldname": "create_variants_automatically_on_configurator_creation", + "fieldtype": "Check", + "label": "Create variants automatically on Configurator Creation" + }, + { + "fieldname": "publish_variants_for_all_templates", + "fieldtype": "Button", + "label": "Publish Variants for All Templates" + }, + { + "fieldname": "section_break_ekez", + "fieldtype": "Section Break" + }, + { + "fieldname": "based_on_attribute", + "fieldtype": "Link", + "label": "Based on Attribute", + "options": "Item Attribute" + }, + { + "fieldname": "communication_tab", + "fieldtype": "Tab Break", + "label": "Communication" + }, + { + "fieldname": "order_section", + "fieldtype": "Section Break", + "label": "Order" + }, + { + "fieldname": "order_confirmation_email_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Order Confirmation Email Template", + "options": "Email Template", + "reqd": 1 + }, + { + "fieldname": "column_break_tald", + "fieldtype": "Column Break" + }, + { + "fieldname": "order_cancellation_email_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Order Cancellation Email Template", + "options": "Email Template", + "reqd": 1 + }, + { + "fieldname": "logo_url", + "fieldtype": "Data", + "label": "Logo URL", + "options": "URL" + }, + { + "fieldname": "item_in_stock_email_template", + "fieldtype": "Link", + "label": "Item In Stock Email Template", + "options": "Email Template", + "reqd": 1 + }, + { + "fieldname": "item_group_section", + "fieldtype": "Section Break", + "label": "Item Group" + }, + { + "fieldname": "contact_information_tab", + "fieldtype": "Tab Break", + "label": "Contact Information" + }, + { + "fieldname": "contact_phone", + "fieldtype": "Data", + "label": "Contact Phone" + }, + { + "fieldname": "column_break_contact", + "fieldtype": "Column Break" + }, + { + "fieldname": "contact_email", + "fieldtype": "Data", + "label": "Contact Email", + "options": "Email" + }, + { + "fieldname": "working_hours", + "fieldtype": "Data", + "label": "Working Hours" + }, + { + "fieldname": "social_media_tab", + "fieldtype": "Tab Break", + "label": "Social Media" + }, + { + "fieldname": "facebook_url", + "fieldtype": "Data", + "label": "Facebook URL", + "options": "URL" + }, + { + "fieldname": "twitter_url", + "fieldtype": "Data", + "label": "Twitter/X URL", + "options": "URL" + }, + { + "fieldname": "column_break_social", + "fieldtype": "Column Break" + }, + { + "fieldname": "instagram_url", + "fieldtype": "Data", + "label": "Instagram URL", + "options": "URL" + }, + { + "fieldname": "snapchat_url", + "fieldtype": "Data", + "label": "Snapchat URL", + "options": "URL" + }, + { + "fieldname": "column_break_social2", + "fieldtype": "Column Break" + }, + { + "fieldname": "tiktok_url", + "fieldtype": "Data", + "label": "TikTok URL", + "options": "URL" + }, + { + "fieldname": "footer_customization_tab", + "fieldtype": "Tab Break", + "label": "Footer Customization" + }, + { + "fieldname": "newsletter_title", + "fieldtype": "Data", + "label": "Newsletter Title" + }, + { + "fieldname": "column_break_newsletter", + "fieldtype": "Column Break" + }, + { + "fieldname": "newsletter_description", + "fieldtype": "Text", + "label": "Newsletter Description" + }, + { + "fieldname": "section_break_footer_assets", + "fieldtype": "Section Break", + "label": "Footer Assets" + }, + { + "fieldname": "payment_methods_image", + "fieldtype": "Attach Image", + "label": "Payment Methods Image" + }, + { + "fieldname": "column_break_footer_assets", + "fieldtype": "Column Break" + }, + { + "fieldname": "vat_certificate_image", + "fieldtype": "Attach Image", + "label": "VAT Certificate Image" + }, + { + "fieldname": "section_break_copyright", + "fieldtype": "Section Break", + "label": "Copyright & Legal" + }, + { + "fieldname": "copyright_text", + "fieldtype": "Data", + "label": "Copyright Text" + }, + { + "fieldname": "section_break_footer_sections", + "fieldtype": "Section Break", + "label": "Footer Sections" + }, + { + "fieldname": "footer_sections", + "fieldtype": "Table", + "label": "Footer Sections", + "options": "Footer Section Mapping" + }, + { + "fieldname": "color_scheme_tab", + "fieldtype": "Tab Break", + "label": "Color Scheme" + }, + { + "default": "#b91c1c", + "fieldname": "primary_color", + "fieldtype": "Color", + "label": "Primary Color" + }, + { + "default": "#991b1b", + "fieldname": "primary_hover_color", + "fieldtype": "Color", + "label": "Primary Hover Color" + }, + { + "fieldname": "column_break_colors1", + "fieldtype": "Column Break" + }, + { + "default": "#7f1d1d", + "fieldname": "link_color", + "fieldtype": "Color", + "label": "Link Color" + }, + { + "default": "#991b1b", + "fieldname": "link_hover_color", + "fieldtype": "Color", + "label": "Link Hover Color" + }, + { + "fieldname": "column_break_colors2", + "fieldtype": "Column Break" + }, + { + "default": "#b91c1c", + "fieldname": "accent_color", + "fieldtype": "Color", + "label": "Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "border_accent_color", + "fieldtype": "Color", + "label": "Border Accent Color" + }, + { + "fieldname": "section_break_ui_colors", + "fieldtype": "Section Break", + "label": "UI Element Colors" + }, + { + "default": "#b91c1c", + "fieldname": "button_bg_color", + "fieldtype": "Color", + "label": "Button Background Color" + }, + { + "fieldname": "column_break_ui_colors1", + "fieldtype": "Column Break" + }, + { + "default": "#b91c1c", + "fieldname": "strikethrough_color", + "fieldtype": "Color", + "label": "Strikethrough Text Color" + }, + { + "default": "#b91c1c", + "fieldname": "badge_bg_color", + "fieldtype": "Color", + "label": "Badge Background Color" + }, + { + "fieldname": "column_break_ui_colors2", + "fieldtype": "Column Break" + }, + { + "default": "#991b1b", + "fieldname": "heading_accent_color", + "fieldtype": "Color", + "label": "Heading Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "brand_text_color", + "fieldtype": "Color", + "label": "Brand Text Color" + }, + { + "default": "#991b1b", + "fieldname": "secondary_accent_color", + "fieldtype": "Color", + "label": "Secondary Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "form_accent_color", + "fieldtype": "Color", + "label": "Form Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "focus_ring_color", + "fieldtype": "Color", + "label": "Focus Ring Color" + }, + { + "fieldname": "section_break_footer_colors", + "fieldtype": "Section Break", + "label": "Footer Colors" + }, + { + "default": "#111827", + "fieldname": "footer_bg_color", + "fieldtype": "Color", + "label": "Footer Background Color" + }, + { + "fieldname": "column_break_footer_colors", + "fieldtype": "Column Break" + }, + { + "default": "#ffffff", + "fieldname": "footer_text_color", + "fieldtype": "Color", + "label": "Footer Text Color" + }, + { + "fieldname": "ecommerce_item_group_mapping", + "fieldtype": "Table", + "label": "eCommerce Item Group Mapping", + "options": "Item Group Map" + }, + { + "fieldname": "sync_item_group_mapping_to_ecommerce_items", + "fieldtype": "Button", + "label": "Sync Item Group Mapping to Existing Items" + }, + { + "fieldname": "payment_mode_section", + "fieldtype": "Section Break", + "label": "Payment Mode" + }, + { + "fieldname": "column_break_iirq", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_oopm", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "telr_enabled", + "fieldtype": "Check", + "label": "Telr" + }, + { + "default": "1", + "fieldname": "tabby_enabled", + "fieldtype": "Check", + "label": "Tabby" + }, + { + "default": "1", + "fieldname": "cod_enabled", + "fieldtype": "Check", + "label": "COD" + }, + { + "default": "0", + "fieldname": "stripe_enabled", + "fieldtype": "Check", + "label": "Stripe" + }, + { + "fieldname": "email_section", + "fieldtype": "Section Break", + "label": "Email" + }, + { + "fieldname": "cc_email", + "fieldtype": "Data", + "label": "CC Email", + "options": "Email" + }, + { + "fieldname": "attribute_name_field", + "fieldtype": "Data", + "label": "Attribute Name Field" + }, + { + "fieldname": "view_logs", + "fieldtype": "Button", + "label": "View Logs" + }, + { + "fieldname": "demo_data_section", + "fieldtype": "Section Break", + "label": "Demo Data & Testing" + }, + { + "fieldname": "install_demo_data", + "fieldtype": "Button", + "label": "Install Demo Data" + }, + { + "fieldname": "column_break_demo", + "fieldtype": "Column Break" + }, + { + "fieldname": "publish_all_items", + "fieldtype": "Button", + "label": "Publish All Items to Website" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2025-10-07 15:34:41.977684", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Lifestyle Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/__init__.py b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.json b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.json new file mode 100644 index 0000000..c8f6633 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.json @@ -0,0 +1,178 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2026-02-16 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "column_break_1", + "ref_docname", + "stripe_session_id", + "status", + "reference_section", + "section_break_amount", + "amount", + "column_break_2", + "currency_code", + "stripe_session_url", + "customer_details_section", + "customer_name", + "column_break_3", + "customer_email", + "invoice_section", + "stripe_payment_intent", + "column_break_4", + "transaction_reference", + "payment_method", + "refund_section", + "refund_amount" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference DocType", + "options": "DocType" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "ref_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "ref_doctype" + }, + { + "fieldname": "stripe_session_id", + "fieldtype": "Data", + "label": "Stripe Session ID", + "read_only": 1 + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Pending\nPaid\nNot Paid\nCancelled\nExpired\nPartially Refunded\nRefunded" + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "section_break_amount", + "fieldtype": "Section Break", + "label": "Amount" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency_code", + "fieldtype": "Link", + "label": "Currency Code", + "options": "Currency" + }, + { + "fieldname": "stripe_session_url", + "fieldtype": "Small Text", + "label": "Stripe Session URL", + "read_only": 1 + }, + { + "fieldname": "customer_details_section", + "fieldtype": "Section Break", + "label": "Customer Details" + }, + { + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "customer_email", + "fieldtype": "Data", + "label": "Customer Email" + }, + { + "fieldname": "invoice_section", + "fieldtype": "Section Break", + "label": "Transaction Details" + }, + { + "fieldname": "stripe_payment_intent", + "fieldtype": "Data", + "label": "Stripe Payment Intent", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "transaction_reference", + "fieldtype": "Data", + "label": "Transaction Reference", + "read_only": 1 + }, + { + "fieldname": "payment_method", + "fieldtype": "Data", + "label": "Payment Method", + "read_only": 1 + }, + { + "fieldname": "refund_section", + "fieldtype": "Section Break", + "label": "Refund" + }, + { + "fieldname": "refund_amount", + "fieldtype": "Currency", + "label": "Refund Amount", + "read_only": 1 + } + ], + "issingle": 0, + "links": [], + "modified": "2026-02-16 12:00:00.000000", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Stripe Payment Request", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.py b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.py new file mode 100644 index 0000000..3531fdb --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_payment_request/stripe_payment_request.py @@ -0,0 +1,92 @@ +# Copyright (c) 2026, and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class StripePaymentRequest(Document): + def before_save(self): + if not self.stripe_session_id: + self.create_session_on_stripe() + + def create_session_on_stripe(self): + """Create a Stripe Checkout Session.""" + stripe_settings = frappe.get_cached_doc("Stripe Settings") + currency_code = self.currency_code or stripe_settings.currency or "SAR" + + session_data = stripe_settings.create_checkout_session( + amount=self.amount, + reference_id=self.name or frappe.generate_hash(length=10), + currency_code=currency_code, + customer_email=self.customer_email, + line_item_name=f"Order - {self.ref_docname}", + ) + + self.stripe_session_id = session_data.get("id") + self.stripe_session_url = session_data.get("url") + self.stripe_payment_intent = session_data.get("payment_intent") + + def sync_status(self): + """Sync payment status from Stripe.""" + stripe_settings = frappe.get_cached_doc("Stripe Settings") + session_data = stripe_settings.get_session_status(self.stripe_session_id) + + payment_status = session_data.get("payment_status") + stripe_status = session_data.get("status") + + # Map Stripe status to our status + if payment_status == "paid": + self.status = "Paid" + elif stripe_status == "expired": + self.status = "Expired" + elif stripe_status == "complete" and payment_status == "unpaid": + self.status = "Not Paid" + elif stripe_status == "open": + self.status = "Pending" + else: + self.status = "Not Paid" + + # Update payment intent + self.stripe_payment_intent = session_data.get("payment_intent") + + # Get payment method details if available + if self.stripe_payment_intent and payment_status == "paid": + self.transaction_reference = self.stripe_payment_intent + + # Check refund status + if self.refund_amount and self.refund_amount > 0: + if self.refund_amount >= self.amount: + self.status = "Refunded" + else: + self.status = "Partially Refunded" + + self.flags.ignore_permissions = True + self.save() + + def refund(self, amount=None): + """Process a refund via Stripe.""" + if not self.stripe_payment_intent: + frappe.throw(_("No payment intent found. Cannot process refund.")) + + stripe_settings = frappe.get_cached_doc("Stripe Settings") + refund_data = stripe_settings.refund_payment( + payment_intent_id=self.stripe_payment_intent, + amount=amount, + ) + + refund_amount = refund_data.get("amount", 0) / 100 # Convert from subunit + self.refund_amount = (self.refund_amount or 0) + refund_amount + self.sync_status() + + +def refund_payment_for_payment_entry(doc, event=None): + """Hook: Auto-refund Stripe payment when a return Payment Entry is submitted.""" + if doc.mode_of_payment != "Stripe" or doc.payment_type != "Pay": + return + + payment_request = frappe.get_doc( + "Stripe Payment Request", {"stripe_session_id": doc.reference_no} + ) + payment_request.refund(doc.paid_amount) diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/__init__.py b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.json b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.json new file mode 100644 index 0000000..7bbee62 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.json @@ -0,0 +1,98 @@ +{ + "actions": [], + "creation": "2026-02-16 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "publishable_key", + "column_break_1", + "secret_key", + "currency", + "section_break_2", + "test_mode", + "column_break_3", + "redirection_urls_section", + "success_url", + "column_break_4", + "cancel_url" + ], + "fields": [ + { + "fieldname": "publishable_key", + "fieldtype": "Data", + "label": "Publishable Key", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "secret_key", + "fieldtype": "Password", + "label": "Secret Key", + "reqd": 1 + }, + { + "fieldname": "currency", + "fieldtype": "Data", + "label": "Currency Code", + "default": "SAR" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "test_mode", + "fieldtype": "Check", + "label": "Test Mode?" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "redirection_urls_section", + "fieldtype": "Section Break", + "label": "Redirection URLs" + }, + { + "fieldname": "success_url", + "fieldtype": "Data", + "label": "Success URL" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "cancel_url", + "fieldtype": "Data", + "label": "Cancel URL" + } + ], + "issingle": 1, + "links": [], + "modified": "2026-02-16 12:00:00.000000", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Stripe Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.py b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.py new file mode 100644 index 0000000..13aa1ee --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/stripe_settings/stripe_settings.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026, and contributors +# For license information, please see license.txt + +import frappe +import requests +from frappe import _ +from frappe.model.document import Document + +STRIPE_API_BASE = "https://api.stripe.com/v1" + + +class StripeSettings(Document): + def create_checkout_session(self, amount, reference_id, currency_code, customer_email, line_item_name="Order Payment"): + """Create a Stripe Checkout Session and return session data.""" + secret_key = self.get_password("secret_key").strip() + + # Stripe expects amount in smallest currency unit (e.g., cents/halalas) + amount_in_subunit = int(round(amount * 100)) + + success_url = self.success_url or frappe.utils.get_url( + f"/en/account/orders/confirmation?payment_mode=stripe&reference_id={reference_id}" + ) + # Append session_id to success URL + if "?" in success_url: + success_url += "&session_id={CHECKOUT_SESSION_ID}" + else: + success_url += "?session_id={CHECKOUT_SESSION_ID}" + + cancel_url = self.cancel_url or frappe.utils.get_url(f"/en/cart/checkout") + + payload = { + "payment_method_types[]": "card", + "mode": "payment", + "success_url": success_url, + "cancel_url": cancel_url, + "client_reference_id": str(reference_id), + "customer_email": customer_email, + "line_items[0][price_data][currency]": currency_code.lower(), + "line_items[0][price_data][unit_amount]": str(amount_in_subunit), + "line_items[0][price_data][product_data][name]": line_item_name, + "line_items[0][quantity]": "1", + } + + response = requests.post( + f"{STRIPE_API_BASE}/checkout/sessions", + data=payload, + auth=(secret_key, ""), + ) + + if response.status_code != 200: + frappe.log_error( + title="Stripe Checkout Session Error", + message=response.text, + ) + frappe.throw(_("Failed to create Stripe checkout session. Please try again.")) + + return response.json() + + def get_session_status(self, session_id): + """Retrieve Stripe Checkout Session status.""" + secret_key = self.get_password("secret_key").strip() + + response = requests.get( + f"{STRIPE_API_BASE}/checkout/sessions/{session_id}", + auth=(secret_key, ""), + ) + + if response.status_code != 200: + frappe.log_error( + title="Stripe Session Status Error", + message=response.text, + ) + frappe.throw(_("Failed to retrieve Stripe session status.")) + + return response.json() + + def refund_payment(self, payment_intent_id, amount=None): + """Create a refund for a Stripe payment.""" + secret_key = self.get_password("secret_key").strip() + + payload = { + "payment_intent": payment_intent_id, + } + + if amount: + payload["amount"] = int(round(amount * 100)) + + response = requests.post( + f"{STRIPE_API_BASE}/refunds", + data=payload, + auth=(secret_key, ""), + ) + + if response.status_code != 200: + frappe.log_error( + title="Stripe Refund Error", + message=response.text, + ) + frappe.throw(_("Failed to process Stripe refund.")) + + return response.json() diff --git a/ls_shop/templates/components/user_dropdown.html b/ls_shop/templates/components/user_dropdown.html index 1426ae4..4da582d 100644 --- a/ls_shop/templates/components/user_dropdown.html +++ b/ls_shop/templates/components/user_dropdown.html @@ -7,7 +7,7 @@ position="right", width="w-40", link='account/dashboard', - + skip_mobile_redirect=true if frappe.session.user == 'Guest' else false, ) %} diff --git a/ls_shop/templates/macros/dropdown.html b/ls_shop/templates/macros/dropdown.html index a56a166..92a5189 100644 --- a/ls_shop/templates/macros/dropdown.html +++ b/ls_shop/templates/macros/dropdown.html @@ -9,14 +9,15 @@ panel_classes="", badge_count=0, show_badge=false, - link='' + link='', + skip_mobile_redirect=false ) %}
{{ _('Checking payment status') const urlParams = new URLSearchParams(window.location.search); let payment_mode = urlParams.get('payment_mode'); const payment_id = urlParams.get('payment_id'); + const session_id = urlParams.get('session_id'); let reference_id = urlParams.get('reference_id'); + + // Handle Stripe redirect (comes back with session_id) + if (session_id && !payment_mode) { + payment_mode = 'stripe'; + } if (!payment_mode) { payment_mode = 'tabby' } - if(!reference_id){ - reference_id = payment_id + + // For Stripe, always use session_id as the reference + if (payment_mode === 'stripe' && session_id) { + reference_id = session_id; + } + + if (!reference_id) { + reference_id = payment_id; } + frappe_call( "/api/v2/method/ls_shop.api.payments.confirm_payment", { payment_mode, reference_id } @@ -28,4 +41,4 @@

{{ _('Checking payment status') } }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ls_shop/www/cart/checkout.html b/ls_shop/www/cart/checkout.html index d4d5308..27795d0 100644 --- a/ls_shop/www/cart/checkout.html +++ b/ls_shop/www/cart/checkout.html @@ -19,6 +19,7 @@ { key: 'telr', enabled: parseInt('{{ show_telr }}') }, { key: 'tabby', enabled: parseInt('{{ show_tabby }}') }, { key: 'cod', enabled: parseInt('{{ show_cod }}') }, + { key: 'stripe', enabled: parseInt('{{ show_stripe }}') }, ] } document.addEventListener('alpine:init', () => { @@ -175,6 +176,9 @@ } window.location.href = data["payment_request"]["tabby_order_url"] } + if (state.payment_mode === "stripe") { + window.location.href = data["payment_request"]["stripe_session_url"] + } } catch (err) { btn.disabled = false; @@ -500,6 +504,21 @@

+