From 9a7e3fe740ceec86f00c902128fc44b14c8f2a06 Mon Sep 17 00:00:00 2001 From: Casey Date: Tue, 3 Feb 2026 17:04:04 -0600 Subject: [PATCH] update estimate page, add stripe script and docker compose for mail server --- custom_ui/api/db/estimates.py | 22 +- custom_ui/api/public/payments.py | 40 +- custom_ui/install.py | 59 ++ custom_ui/models/__init__.py | 1 + custom_ui/models/payments.py | 10 + custom_ui/services/__init__.py | 4 +- custom_ui/services/estimate_service.py | 17 +- custom_ui/services/item_service.py | 38 ++ custom_ui/services/payment_service.py | 46 +- custom_ui/services/sales_order_service.py | 7 +- docker-compose.local.yaml | 8 + frontend/src/api.js | 4 +- .../src/components/common/ItemSelector.vue | 145 +++++ .../src/components/modals/AddItemModal.vue | 526 ++++++++++++++++++ .../components/modals/BidMeetingNoteForm.vue | 42 +- stripe-init-webhook.sh | 115 ++++ stripe-local-init.sh | 150 +++++ 17 files changed, 1168 insertions(+), 66 deletions(-) create mode 100644 custom_ui/models/__init__.py create mode 100644 custom_ui/models/payments.py create mode 100644 custom_ui/services/item_service.py create mode 100644 docker-compose.local.yaml create mode 100644 frontend/src/components/common/ItemSelector.vue create mode 100644 frontend/src/components/modals/AddItemModal.vue create mode 100644 stripe-init-webhook.sh create mode 100755 stripe-local-init.sh diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 678200b..8aadf5a 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -4,7 +4,7 @@ from custom_ui.api.db.general import get_doc_history from custom_ui.db_utils import DbUtils, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response from werkzeug.wrappers import Response from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer -from custom_ui.services import DbService, ClientService, AddressService, ContactService +from custom_ui.services import DbService, ClientService, AddressService, ContactService, EstimateService, ItemService # =============================================================================== # ESTIMATES & INVOICES API METHODS @@ -86,11 +86,25 @@ def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10): @frappe.whitelist() -def get_quotation_items(): +def get_quotation_items(project_template:str = None): """Get all available quotation items.""" try: - items = frappe.get_all("Item", fields=["*"], filters={"item_group": "SNW-S"}) - return build_success_response(items) + filters = EstimateService.map_project_template_to_filter(project_template) + items = frappe.get_all("Item", fields=["item_code", "item_group"], filters=filters) + grouped_item_dicts = {} + for item in items: + item_dict = ItemService.get_full_dict(item.item_code) + if item_dict["bom"]: + if "Packages" not in grouped_item_dicts: + grouped_item_dicts["Packages"] = {} + if item.item_group not in grouped_item_dicts["Packages"]: + grouped_item_dicts["Packages"][item.item_group] = [] + grouped_item_dicts["Packages"][item.item_group].append(item_dict) + else: + if item.item_group not in grouped_item_dicts: + grouped_item_dicts[item.item_group] = [] + grouped_item_dicts[item.item_group].append(item_dict) + return build_success_response(grouped_item_dicts) except Exception as e: return build_error_response(str(e), 500) diff --git a/custom_ui/api/public/payments.py b/custom_ui/api/public/payments.py index b369087..9f4a88a 100644 --- a/custom_ui/api/public/payments.py +++ b/custom_ui/api/public/payments.py @@ -1,7 +1,8 @@ import frappe import json from frappe.utils.data import flt -from custom_ui.services import DbService, StripeService +from custom_ui.services import DbService, StripeService, PaymentService +from custom_ui.models import PaymentData @frappe.whitelist(allow_guest=True) def half_down_stripe_payment(sales_order): @@ -31,37 +32,36 @@ def stripe_webhook(): sig_header = frappe.request.headers.get('Stripe-Signature') session, metadata = StripeService.get_session_and_metadata(payload, sig_header) + required_keys = ["order_num", "company", "payment_type"] + for key in required_keys: + if not metadata.get(key): + raise frappe.ValidationError(f"Missing required metadata key: {key}") + if DbService.exists("Payment Entry", {"reference_no": session.id}): raise frappe.ValidationError("Payment Entry already exists for this session.") - reference_doctype = "Sales Invoice" + + reference_doctype = "Sales Invoice" if metadata.get("payment_type") == "advance": reference_doctype = "Sales Order" elif metadata.get("payment_type") != "full": raise frappe.ValidationError("Invalid payment type in metadata.") amount_paid = flt(session.amount_total) / 100 - currency = session.currency.upper() - reference_doc = frappe.get_doc(reference_doctype, metadata.get("order_num")) - pe = frappe.get_doc({ - "doctype": "Payment Entry", - "payment_type": "Receive", - "party_type": "Customer", - "mode_of_payment": "Stripe", - "party": reference_doc.customer, - "party_name": reference_doc.customer, - "paid_to": metadata.get("company"), - "reference_no": session.id, - "reference_date": frappe.utils.nowdate(), - "reference_doctype": reference_doctype, - "reference_name": reference_doc.name, - "paid_amount": amount_paid, - "paid_currency": currency, - }) + # stripe_settings = StripeService.get_stripe_settings(metadata.get("company")) - pe.insert() + pe = PaymentService.create_payment_entry( + data=PaymentData( + mode_of_payment="Stripe", + reference_no=session.id, + reference_date=session.created, + received_amount=amount_paid, + company=metadata.get("company"), + reference_doc_name=metadata.get("order_num") + ) + ) pe.submit() return "Payment Entry created and submitted successfully." diff --git a/custom_ui/install.py b/custom_ui/install.py index c4a42ae..450e82d 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -24,6 +24,8 @@ def after_install(): create_task_types() # create_tasks() create_bid_meeting_note_form_templates() + create_accounts() + init_stripe_accounts() build_frontend() def after_migrate(): @@ -42,6 +44,8 @@ def after_migrate(): create_task_types() # create_tasks() create_bid_meeting_note_form_templates() + create_accounts() + init_stripe_accounts() # update_address_fields() # build_frontend() @@ -1378,3 +1382,58 @@ def create_bid_meeting_note_form_templates(): ) doc.insert(ignore_permissions=True) + +def create_accounts(): + """Create necessary accounts if they do not exist.""" + print("\nšŸ”§ Checking for necessary accounts...") + + accounts = [ + { + "Sprinklers Northwest": [ + { + "account_name": "Stripe Clearing - Sprinklers Northwest", + "account_type": "Bank", + "parent_account": "Bank Accounts - S", + "company": "Sprinklers Northwest" + } + ] + } + ] + + for company_accounts in accounts: + for company, account_list in company_accounts.items(): + for account in account_list: + # Idempotency check + if frappe.db.exists("Account", {"account_name": account["account_name"], "company": account["company"]}): + continue + doc = frappe.get_doc({ + "doctype": "Account", + "account_name": account["account_name"], + "account_type": account["account_type"], + "company": account["company"], + "parent_account": account["parent_account"], + "is_group": 0 + }) + doc.insert(ignore_permissions=True) + + frappe.db.commit() + +def init_stripe_accounts(): + """Initializes the bare configurations for each Stripe Settings doctypes.""" + print("\nšŸ”§ Initializing Stripe Settings for companies...") + + companies = ["Sprinklers Northwest"] + + for company in companies: + if not frappe.db.exists("Stripe Settings", {"company": company}): + doc = frappe.get_doc({ + "doctype": "Stripe Settings", + "company": company, + "api_key": "", + "publishable_key": "", + "webhook_secret": "", + "account": f"Stripe Clearing - {company}" + }) + doc.insert(ignore_permissions=True) + + frappe.db.commit() diff --git a/custom_ui/models/__init__.py b/custom_ui/models/__init__.py new file mode 100644 index 0000000..c0134af --- /dev/null +++ b/custom_ui/models/__init__.py @@ -0,0 +1 @@ +from .payments import PaymentData \ No newline at end of file diff --git a/custom_ui/models/payments.py b/custom_ui/models/payments.py new file mode 100644 index 0000000..dfd9bf4 --- /dev/null +++ b/custom_ui/models/payments.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +@dataclass +class PaymentData: + mode_of_payment: str + reference_no: str + reference_date: str + received_amount: float + company: str = None + reference_doc_name: str = None \ No newline at end of file diff --git a/custom_ui/services/__init__.py b/custom_ui/services/__init__.py index a1d4631..d5fafc8 100644 --- a/custom_ui/services/__init__.py +++ b/custom_ui/services/__init__.py @@ -6,4 +6,6 @@ from .estimate_service import EstimateService from .onsite_meeting_service import OnSiteMeetingService from .task_service import TaskService from .service_appointment_service import ServiceAppointmentService -from .stripe_service import StripeService \ No newline at end of file +from .stripe_service import StripeService +from .payment_service import PaymentService +from .item_service import ItemService \ No newline at end of file diff --git a/custom_ui/services/estimate_service.py b/custom_ui/services/estimate_service.py index d5e3cf2..ea05a8a 100644 --- a/custom_ui/services/estimate_service.py +++ b/custom_ui/services/estimate_service.py @@ -1,4 +1,5 @@ import frappe +from .item_service import ItemService class EstimateService: @@ -93,4 +94,18 @@ class EstimateService: estimate_doc.customer = customer_name estimate_doc.save(ignore_permissions=True) print(f"DEBUG: Linked Quotation {estimate_doc.name} to {customer_type} {customer_name}") - \ No newline at end of file + + @staticmethod + def map_project_template_to_filter(project_template: str = None) -> dict | None: + """Map a project template to a filter.""" + print(f"DEBUG: Mapping project template {project_template} to quotation category") + if not project_template: + print("DEBUG: No project template provided, defaulting to 'General'") + return None + mapping = { + # SNW Install is both Irrigation and SNW-S categories + "SNW Install": ["in", ["Irrigation", "SNW-S", "Landscaping"]], + } + category = mapping.get(project_template, "General") + print(f"DEBUG: Mapped to quotation category: {category}") + return { "item_group": category } \ No newline at end of file diff --git a/custom_ui/services/item_service.py b/custom_ui/services/item_service.py new file mode 100644 index 0000000..4e4e3de --- /dev/null +++ b/custom_ui/services/item_service.py @@ -0,0 +1,38 @@ +import frappe + +class ItemService: + + @staticmethod + def get_item_category(item_code: str) -> str: + """Retrieve the category of an Item document by item code.""" + print(f"DEBUG: Getting category for Item {item_code}") + category = frappe.db.get_value("Item", item_code, "item_group") + print(f"DEBUG: Retrieved category: {category}") + return category + + @staticmethod + def get_full_dict(item_code: str) -> frappe._dict: + """Retrieve the full Item document by item code.""" + print(f"DEBUG: Getting full document for Item {item_code}") + item_doc = frappe.get_doc("Item", item_code).as_dict() + item_doc["bom"] = ItemService.get_full_bom_dict(item_code) if item_doc.get("default_bom") else None + return item_doc + + @staticmethod + def get_full_bom_dict(item_code: str): + """Retrieve the Bill of Materials (BOM) associated with an Item.""" + print(f"DEBUG: Getting BOM for Item {item_code}") + bom_name = frappe.db.get_value("BOM", {"item": item_code, "is_active": 1}, "name") + bom_dict = frappe.get_doc("BOM", bom_name).as_dict() + for item in bom_dict.get('exploded_items', []): + item_bom_name = frappe.get_value("Item", item["item_name"], "default_bom") + item["bom"] = frappe.get_doc("BOM", item_bom_name).as_dict() if item_bom_name else None + return bom_dict + + @staticmethod + def exists(item_code: str) -> bool: + """Check if an Item document exists by item code.""" + print(f"DEBUG: Checking existence of Item {item_code}") + exists = frappe.db.exists("Item", item_code) is not None + print(f"DEBUG: Item {item_code} exists: {exists}") + return exists \ No newline at end of file diff --git a/custom_ui/services/payment_service.py b/custom_ui/services/payment_service.py index ca447f1..db84f18 100644 --- a/custom_ui/services/payment_service.py +++ b/custom_ui/services/payment_service.py @@ -1,29 +1,51 @@ import frappe -from custom_ui.services import DbService +from custom_ui.services import DbService, StripeService +from dataclasses import dataclass +from custom_ui.models import PaymentData + + class PaymentService: @staticmethod - def create_payment_entry(reference_doctype: str, reference_doc_name: str, data: dict) -> frappe._dict: + def create_payment_entry(data: PaymentData) -> frappe._dict: """Create a Payment Entry document based on the reference document.""" - print(f"DEBUG: Creating Payment Entry for {reference_doctype} {reference_doc_name} with data: {data}") - reference_doc = DbService.get_or_throw(reference_doctype, reference_doc_name) + print(f"DEBUG: Creating Payment Entry for {data.reference_doc_name} with data: {data}") + reference_doctype = PaymentService.determine_reference_doctype(data.reference_doc_name) + reference_doc = DbService.get_or_throw(reference_doctype, data.reference_doc_name) + account = StripeService.get_stripe_settings(data.company).account pe = frappe.get_doc({ "doctype": "Payment Entry", + "company": data.company, "payment_type": "Receive", "party_type": "Customer", - "mode_of_payment": data.get("mode_of_payment", "Stripe"), + "mode_of_payment": data.mode_of_payment or "Stripe", "party": reference_doc.customer, "party_name": reference_doc.customer, - "paid_to": data.get("paid_to"), - "reference_no": data.get("reference_no"), - "reference_date": data.get("reference_date", frappe.utils.nowdate()), - "reference_doctype": reference_doctype, + "paid_to": account, + "reference_no": data.reference_no, + "reference_date": data.reference_date or frappe.utils.nowdate(), + "received_amount": data.received_amount, + "paid_currency": "USD", + "received_currency": "USD", + }).append("references", { + "reference_doctype": reference_doc.doctype, "reference_name": reference_doc.name, - "paid_amount": data.get("paid_amount"), - "paid_currency": data.get("paid_currency"), + "reconcile_effect_on": reference_doc.doctype, + "allocated_amount": data.received_amount, }) pe.insert() print(f"DEBUG: Created Payment Entry with name: {pe.name}") - return pe.as_dict() + return pe + + @staticmethod + def determine_reference_doctype(reference_doc_name: str) -> str: + """Determine the reference doctype based on the document name pattern.""" + print(f"DEBUG: Determining reference doctype for document name: {reference_doc_name}") + if DbService.exists("Sales Order", reference_doc_name): + return "Sales Order" + elif DbService.exists("Sales Invoice", reference_doc_name): + return "Sales Invoice" + else: + frappe.throw("Unable to determine reference doctype from document name.") \ No newline at end of file diff --git a/custom_ui/services/sales_order_service.py b/custom_ui/services/sales_order_service.py index 9730618..0bdeca3 100644 --- a/custom_ui/services/sales_order_service.py +++ b/custom_ui/services/sales_order_service.py @@ -1,7 +1,2 @@ -import frappe -class SalesOrderService: - - @staticmethod - def apply_advance_payment(sales_order_name: str, payment_entry_doc): - pass \ No newline at end of file + \ No newline at end of file diff --git a/docker-compose.local.yaml b/docker-compose.local.yaml new file mode 100644 index 0000000..b99b3de --- /dev/null +++ b/docker-compose.local.yaml @@ -0,0 +1,8 @@ +services: + mailhog: + image: mailhog/mailhog:latest + container_name: mailhog + ports: + - "8025:8025" # MailHog web UI + - "1025:1025" # SMTP server + restart: unless-stopped \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 01e1baf..a1da164 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -229,8 +229,8 @@ class Api { // ESTIMATE / QUOTATION METHODS // ============================================================================ - static async getQuotationItems() { - return await this.request("custom_ui.api.db.estimates.get_quotation_items"); + static async getQuotationItems(projectTemplate) { + return await this.request("custom_ui.api.db.estimates.get_quotation_items", { projectTemplate }); } static async getEstimateFromAddress(fullAddress) { diff --git a/frontend/src/components/common/ItemSelector.vue b/frontend/src/components/common/ItemSelector.vue new file mode 100644 index 0000000..46ee774 --- /dev/null +++ b/frontend/src/components/common/ItemSelector.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/frontend/src/components/modals/AddItemModal.vue b/frontend/src/components/modals/AddItemModal.vue new file mode 100644 index 0000000..ae70a8a --- /dev/null +++ b/frontend/src/components/modals/AddItemModal.vue @@ -0,0 +1,526 @@ + + + + + diff --git a/frontend/src/components/modals/BidMeetingNoteForm.vue b/frontend/src/components/modals/BidMeetingNoteForm.vue index 8783a14..113361a 100644 --- a/frontend/src/components/modals/BidMeetingNoteForm.vue +++ b/frontend/src/components/modals/BidMeetingNoteForm.vue @@ -20,24 +20,27 @@
@@ -573,7 +575,7 @@ const loadDoctypeOptions = async () => { for (const field of fieldsWithDoctype) { try { // Use the new API method for fetching docs - let docs = await Api.getQuotationItems(); + let docs = await Api.getQuotationItems(props.projectTemplate); // Deduplicate by value field const valueField = field.doctypeValueField || 'name'; diff --git a/stripe-init-webhook.sh b/stripe-init-webhook.sh new file mode 100644 index 0000000..4b6952d --- /dev/null +++ b/stripe-init-webhook.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Script to initialize and run Stripe CLI webhook forwarding +# Usage: ./stripe-init-webhook.sh --site --port + +set -e + +# Default values +SITE="" +PORT="8000" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --site) + SITE="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 --site [--port ]" + echo "" + echo "Options:" + echo " --site Required. The site domain (e.g., erp.local)" + echo " --port Optional. The port number (default: 8000)" + echo "" + echo "Example:" + echo " $0 --site erp.local --port 8000" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Check if required flag is provided +if [ -z "$SITE" ]; then + echo "Error: --site flag is required" + echo "Usage: $0 --site [--port ]" + exit 1 +fi + +echo "Checking Stripe CLI installation..." + +# Check if Stripe CLI is installed +if ! command -v stripe &> /dev/null; then + echo "Stripe CLI is not installed." + read -p "Would you like to install it now? (y/n) " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Installing Stripe CLI..." + + # Add GPG key + echo "Adding Stripe GPG key..." + curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg > /dev/null + + # Add repository + echo "Adding Stripe repository..." + echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list + + # Update and install + echo "Updating package list..." + sudo apt update + + echo "Installing Stripe CLI..." + sudo apt install stripe -y + + echo "Stripe CLI installed successfully!" + else + echo "Installation cancelled. Exiting." + exit 1 + fi +else + echo "Stripe CLI is already installed." +fi + +# Check if Stripe CLI is authenticated +echo "Checking authentication status..." +if ! stripe config --list &> /dev/null; then + echo "Stripe CLI is not authenticated." + echo "Please log in to your Stripe account..." + stripe login --interactive +else + # Try to verify authentication by running a simple command + if stripe config --list | grep -q "test_mode_api_key"; then + echo "Stripe CLI is authenticated." + else + echo "Stripe CLI authentication may be invalid." + echo "Please log in to your Stripe account..." + stripe login --interactive + fi +fi + +# Start Docker containers +echo "" +echo "Starting Docker containers..." +docker compose -f docker-compose.local.yaml up -d + +# Start listening for webhooks +WEBHOOK_URL="http://${SITE}:${PORT}/api/method/custom_ui.api.public.payments.stripe_webhook" +echo "" +echo "Starting Stripe webhook listener..." +echo "Forwarding to: $WEBHOOK_URL" +echo "" +echo "Press Ctrl+C to stop" +echo "" + +stripe listen --forward-to "$WEBHOOK_URL" diff --git a/stripe-local-init.sh b/stripe-local-init.sh new file mode 100755 index 0000000..9c292bf --- /dev/null +++ b/stripe-local-init.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Script to initialize and run Stripe CLI webhook forwarding +# Usage: ./stripe-init-webhook.sh --site --port + +set -e + +# Colors for output +BLUE='\033[0;34m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Display banner +echo "" +echo -e "${BLUE}+-----------------------------------------------+${NC}" +echo -e "${BLUE} ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—${NC}" +echo -e "${BLUE} ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘${NC}" +echo -e "${BLUE} ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘${NC}" +echo -e "${BLUE} ā•šā•ā•ā•ā–ˆā–ˆā•— ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘${NC}" +echo -e "${BLUE} ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā• ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘${NC}" +echo -e "${BLUE} ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•${NC}" +echo -e "${BLUE}+-----------------------------------------------+${NC}" +echo -e "${YELLOW} šŸš€ Automated Local Stripe Environment${NC}" +echo -e "${YELLOW} šŸ’¼ For ERPNext Development${NC}" +echo -e "${BLUE}+-----------------------------------------------+${NC}" +echo "" + +# Default values +SITE="" +PORT="8000" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --site) + SITE="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 --site [--port ]" + echo "" + echo "Options:" + echo " --site Required. The site domain (e.g., erp.local)" + echo " --port Optional. The port number (default: 8000)" + echo "" + echo "Example:" + echo " $0 --site erp.local --port 8000" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Check if required flag is provided +if [ -z "$SITE" ]; then + echo -e "${RED}āŒ Error: --site flag is required${NC}" + echo "Usage: $0 --site [--port ]" + exit 1 +fi + +echo -e "${BLUE}šŸ” Checking Stripe CLI installation...${NC}" + +# Check if Stripe CLI is installed +if ! command -v stripe &> /dev/null; then + echo -e "${YELLOW}āš ļø Stripe CLI is not installed.${NC}" + read -p "Would you like to install it now? (y/n) " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo -e "${BLUE}šŸ“¦ Installing Stripe CLI...${NC}" + + # Add GPG key + echo -e "${BLUE}šŸ”‘ Adding Stripe GPG key...${NC}" + curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg > /dev/null + + # Add repository + echo -e "${BLUE}šŸ“š Adding Stripe repository...${NC}" + echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list + + # Update and install + echo -e "${BLUE}šŸ”„ Updating package list...${NC}" + sudo apt update + + echo -e "${BLUE}ā¬‡ļø Installing Stripe CLI...${NC}" + sudo apt install stripe -y + + echo -e "${GREEN}āœ… Stripe CLI installed successfully!${NC}" + else + echo -e "${RED}āŒ Installation cancelled. Exiting.${NC}" + exit 1 + fi +else + echo -e "${GREEN}āœ… Stripe CLI is already installed.${NC}" +fi + +# Check if Stripe CLI is authenticated +echo -e "${BLUE}šŸ” Checking authentication status...${NC}" +if ! stripe config --list &> /dev/null; then + echo -e "${YELLOW}āš ļø Stripe CLI is not authenticated.${NC}" + echo -e "${BLUE}šŸ”‘ Please log in to your Stripe account...${NC}" + stripe login --interactive +else + # Try to verify authentication by running a simple command + if stripe config --list | grep -q "test_mode_api_key"; then + echo -e "${GREEN}āœ… Stripe CLI is authenticated.${NC}" + else + echo -e "${YELLOW}āš ļø Stripe CLI authentication may be invalid.${NC}" + echo -e "${BLUE}šŸ”‘ Please log in to your Stripe account...${NC}" + stripe login --interactive + fi +fi + +# Start Docker containers +echo "" +echo -e "${BLUE}🐳 Starting Docker containers...${NC}" +docker compose -f docker-compose.local.yaml up -d + +# Cleanup function to run on exit +cleanup() { + echo "" + echo -e "${YELLOW}šŸ›‘ Shutting down...${NC}" + echo -e "${BLUE}🐳 Stopping Docker containers...${NC}" + docker compose -f docker-compose.local.yaml down + echo -e "${GREEN}✨ Cleanup complete. Goodbye!${NC}" + exit 0 +} + +# Trap Ctrl+C (SIGINT) and termination signals +trap cleanup SIGINT SIGTERM EXIT + +# Start listening for webhooks +WEBHOOK_URL="http://${SITE}:${PORT}/api/method/custom_ui.api.public.payments.stripe_webhook" +echo "" +echo -e "${GREEN}šŸŽ§ Starting Stripe webhook listener...${NC}" +echo -e "${BLUE}šŸ“” Forwarding to: ${YELLOW}$WEBHOOK_URL${NC}" +echo "" +echo -e "${YELLOW}āŒØļø Press Ctrl+C to stop${NC}" +echo "" + +stripe listen --forward-to "$WEBHOOK_URL"