From 45e356ca6590c1ef4be28bb228d50a3fbf486fa6 Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Thu, 19 Dec 2024 14:18:19 +0530 Subject: [PATCH 01/12] feat(partner): Introducing Partnership Fees This will allow partners to pay for Partnership renewal Fee directly from FC To enable this a new Invoice type and Balance Transaction Type called as Partnership Fee has been introduced. --- press/api/billing.py | 28 ++++++++ press/api/partner.py | 2 +- .../balance_transaction.json | 4 +- .../balance_transaction.py | 17 ++--- press/press/doctype/invoice/invoice.json | 4 +- press/press/doctype/invoice/invoice.py | 2 +- .../press_settings/press_settings.json | 35 +++++++++- .../doctype/press_settings/press_settings.py | 2 + press/press/doctype/team/team.py | 68 ++++++++++++++++++- 9 files changed, 142 insertions(+), 20 deletions(-) diff --git a/press/api/billing.py b/press/api/billing.py index ff446cffb1..cdd129ddd0 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -83,6 +83,7 @@ def balances(): "source": ("in", ("Prepaid Credits", "Transferred Credits", "Free Credits")), "team": team, "docstatus": 1, + "type": ("!=", "Partnership Fee"), }, limit=1, ) @@ -233,6 +234,33 @@ def create_payment_intent_for_micro_debit(payment_method_name): return {"client_secret": intent["client_secret"]} +@frappe.whitelist() +def create_payment_intent_for_partnership_fees(): + team = get_current_team(True) + press_settings = frappe.get_cached_doc("Press Settings") + metadata = {"payment_for": "partnership_fee"} + fee_amount = press_settings.partnership_fee_usd + + if team.currency == "INR": + fee_amount = press_settings.partnership_fee_inr + gst_amount = fee_amount * press_settings.gst_percentage + fee_amount += gst_amount + metadata.update({"gst": round(gst_amount, 2)}) + + stripe = get_stripe() + intent = stripe.PaymentIntent.create( + amount=int(fee_amount * 100), + currency=team.currency.lower(), + customer=team.stripe_customer_id, + description="Partnership Fee", + metadata=metadata, + ) + return { + "client_secret": intent["client_secret"], + "publishable_key": get_publishable_key(), + } + + @frappe.whitelist() def create_payment_intent_for_buying_credits(amount): team = get_current_team(True) diff --git a/press/api/partner.py b/press/api/partner.py index 2877e725d0..d0446e3a62 100644 --- a/press/api/partner.py +++ b/press/api/partner.py @@ -57,9 +57,9 @@ def get_partner_details(partner_email): "partner_type", "company_name", "custom_ongoing_period_fc_invoice_contribution", - "custom_ongoing_period_enterprise_invoice_contribution", "partner_name", "custom_number_of_certified_members", + "end_date", ], ) if data: diff --git a/press/press/doctype/balance_transaction/balance_transaction.json b/press/press/doctype/balance_transaction/balance_transaction.json index a8fe964622..013dfc90e3 100644 --- a/press/press/doctype/balance_transaction/balance_transaction.json +++ b/press/press/doctype/balance_transaction/balance_transaction.json @@ -33,7 +33,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Adjustment\nApplied To Invoice" + "options": "Adjustment\nApplied To Invoice\nPartnership Fee" }, { "fetch_from": "team.currency", @@ -104,7 +104,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-01-04 22:28:44.740627", + "modified": "2024-12-19 13:06:26.343005", "modified_by": "Administrator", "module": "Press", "name": "Balance Transaction", diff --git a/press/press/doctype/balance_transaction/balance_transaction.py b/press/press/doctype/balance_transaction/balance_transaction.py index 4e7390f1cc..54f59e593c 100644 --- a/press/press/doctype/balance_transaction/balance_transaction.py +++ b/press/press/doctype/balance_transaction/balance_transaction.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe and contributors # For license information, please see license.txt - +from __future__ import annotations import frappe from frappe.model.document import Document @@ -39,20 +38,24 @@ class BalanceTransaction(Document): "Marketplace Consumption", ] team: DF.Link - type: DF.Literal["Adjustment", "Applied To Invoice"] + type: DF.Literal["Adjustment", "Applied To Invoice", "Partnership Fee"] unallocated_amount: DF.Currency # end: auto-generated types - dashboard_fields = ["type", "amount", "ending_balance", "invoice", "source"] + dashboard_fields = ("type", "amount", "ending_balance", "invoice", "source") def validate(self): if self.amount == 0: frappe.throw("Amount cannot be 0") def before_submit(self): + if self.type == "Partnership Fee": + # don't update ending balance or unallocated amount for partnership fee + return + last_balance = frappe.db.get_all( "Balance Transaction", - filters={"team": self.team, "docstatus": 1}, + filters={"team": self.team, "docstatus": 1, "type": ("!=", "Partnership Fee")}, fields=["sum(amount) as ending_balance"], group_by="team", pluck="ending_balance", @@ -133,6 +136,4 @@ def validate_total_unallocated_amount(self): ) -get_permission_query_conditions = get_permission_query_conditions_for_doctype( - "Balance Transaction" -) +get_permission_query_conditions = get_permission_query_conditions_for_doctype("Balance Transaction") diff --git a/press/press/doctype/invoice/invoice.json b/press/press/doctype/invoice/invoice.json index 048b55d0b6..e9b21f6f31 100644 --- a/press/press/doctype/invoice/invoice.json +++ b/press/press/doctype/invoice/invoice.json @@ -302,7 +302,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Subscription\nPrepaid Credits\nService\nSummary" + "options": "Subscription\nPrepaid Credits\nService\nSummary\nPartnership Fees" }, { "depends_on": "eval:doc.type == 'Prepaid Credits'", @@ -494,7 +494,7 @@ "link_fieldname": "invoice" } ], - "modified": "2024-12-16 20:08:37.566622", + "modified": "2024-12-18 22:28:29.793591", "modified_by": "Administrator", "module": "Press", "name": "Invoice", diff --git a/press/press/doctype/invoice/invoice.py b/press/press/doctype/invoice/invoice.py index 143c29c97c..bcfb0681e9 100644 --- a/press/press/doctype/invoice/invoice.py +++ b/press/press/doctype/invoice/invoice.py @@ -82,7 +82,7 @@ class Invoice(Document): transaction_fee: DF.Currency transaction_fee_details: DF.Table[InvoiceTransactionFee] transaction_net: DF.Currency - type: DF.Literal["Subscription", "Prepaid Credits", "Service", "Summary"] + type: DF.Literal["Subscription", "Prepaid Credits", "Service", "Summary", "Partnership Fees"] write_off_amount: DF.Float # end: auto-generated types diff --git a/press/press/doctype/press_settings/press_settings.json b/press/press/doctype/press_settings/press_settings.json index a3e5560bd9..3986eb2b3b 100644 --- a/press/press/doctype/press_settings/press_settings.json +++ b/press/press/doctype/press_settings/press_settings.json @@ -199,12 +199,17 @@ "section_break_jstu", "enable_app_grouping", "default_apps", - "code_spaces_tab", - "spaces_domain", + "partner_tab", + "partnership_fees_section", + "partnership_fee_usd", + "column_break_yxrj", + "partnership_fee_inr", "hybrid_server_tab", "hybrid_cluster", "hybrid_domain", - "tls_renewal_queue_size" + "tls_renewal_queue_size", + "code_spaces_tab", + "spaces_domain" ], "fields": [ { @@ -1285,6 +1290,30 @@ "fieldname": "redis_cache_size", "fieldtype": "Int", "label": "Redis Cache Size (MB)" + }, + { + "fieldname": "partner_tab", + "fieldtype": "Tab Break", + "label": "Partner" + }, + { + "fieldname": "partnership_fees_section", + "fieldtype": "Section Break", + "label": "Partnership Fees" + }, + { + "fieldname": "partnership_fee_usd", + "fieldtype": "Int", + "label": "Partnership Fee USD" + }, + { + "fieldname": "column_break_yxrj", + "fieldtype": "Column Break" + }, + { + "fieldname": "partnership_fee_inr", + "fieldtype": "Int", + "label": "Partnership Fee INR" } ], "issingle": 1, diff --git a/press/press/doctype/press_settings/press_settings.py b/press/press/doctype/press_settings/press_settings.py index c8ad9b2cc0..09d75c568d 100644 --- a/press/press/doctype/press_settings/press_settings.py +++ b/press/press/doctype/press_settings/press_settings.py @@ -104,6 +104,8 @@ class PressSettings(Document): offsite_backups_count: DF.Int offsite_backups_provider: DF.Literal["AWS S3"] offsite_backups_secret_access_key: DF.Password | None + partnership_fee_inr: DF.Int + partnership_fee_usd: DF.Int plausible_api_key: DF.Password | None plausible_site_id: DF.Data | None plausible_url: DF.Data | None diff --git a/press/press/doctype/team/team.py b/press/press/doctype/team/team.py index 0c5a4cdbed..02b719ff2e 100644 --- a/press/press/doctype/team/team.py +++ b/press/press/doctype/team/team.py @@ -811,11 +811,11 @@ def get_past_invoices(self): invoice.stripe_link_expired = True return invoices - def allocate_credit_amount(self, amount, source, remark=None): + def allocate_credit_amount(self, amount, source, remark=None, type="Adjustment"): doc = frappe.get_doc( doctype="Balance Transaction", team=self.name, - type="Adjustment", + type=type, source=source, amount=amount, description=remark, @@ -886,7 +886,7 @@ def invite_team_member(self, email, roles=None): def get_balance(self): res = frappe.get_all( "Balance Transaction", - filters={"team": self.name, "docstatus": 1}, + filters={"team": self.name, "docstatus": 1, "type": ("!=", "Partnership Fee")}, order_by="creation desc", limit=1, pluck="ending_balance", @@ -1313,6 +1313,10 @@ def process_stripe_webhook(doc, method): process_micro_debit_test_charge(event) return + if payment_for and payment_for == "partnership_fee": + process_partnership_fee(payment_intent) + return + handle_payment_intent_succeeded(payment_intent) @@ -1412,6 +1416,64 @@ def enqueue_finalize_unpaid_for_team(team: str): doc.finalize_invoice() +def procees_partnership_fee(payment_intent): + from datetime import datetime + + if isinstance(payment_intent, str): + stripe = get_stripe() + payment_intent = stripe.PaymentIntent.retrieve(payment_intent) + + metadata = payment_intent.get("metadata") + if frappe.db.exists("Invoice", {"stripe_payment_intent_id": payment_intent["id"], "status": "Paid"}): + # ignore creating duplicate partnership fee invoice + return + + team = frappe.get_doc("Team", {"stripe_customer_id": payment_intent["customer"]}) + amount_with_tax = payment_intent["amount"] / 100 + gst = float(metadata.get("gst", 0)) + amount = amount_with_tax - gst + balance_transaction = team.allocate_credit_amount( + amount, source="Prepaid Credits", remark=payment_intent["id"], type="Partnership Fee" + ) + + invoice = frappe.get_doc( + doctype="Invoice", + team=team.name, + type="Partnership Fee", + status="Paid", + due_date=datetime.fromtimestamp(payment_intent["created"]), + total=amount, + amount_due=amount, + gst=gst or 0, + amount_due_with_tax=amount_with_tax, + amount_paid=amount_with_tax, + stripe_payment_intent_id=payment_intent["id"], + ) + invoice.append( + "items", + { + "description": "Partnership Fee", + "document_type": "Balance Transaction", + "document_name": balance_transaction.name, + "quantity": 1, + "rate": amount, + }, + ) + invoice.insert() + invoice.reload() + + # latest stripe API sets charge id in latest_charge + charge = payment_intent.get("latest_charge") + if not charge: + # older stripe API sets charge id in charges.data + charges = payment_intent.get("charges", {}).get("data", []) + charge = charges[0]["id"] if charges else None + if charge: + # update transaction amount, fee and exchange rate + invoice.update_transaction_details(charge) + invoice.submit() + + def get_permission_query_conditions(user): from press.utils import get_current_team From 527d883c94e8664bdeeaf56c62b3544a5e14ba5d Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Mon, 23 Dec 2024 16:53:05 +0530 Subject: [PATCH 02/12] refactor(partner): revamp partner overview --- .../components/billing/PaymentDetails.vue | 12 +- .../partners/PartnerContribution.vue | 2 +- .../components/partners/PartnerOverview.vue | 334 ++++++++++++------ press/press/doctype/team/team.py | 2 +- 4 files changed, 227 insertions(+), 123 deletions(-) diff --git a/dashboard/src2/components/billing/PaymentDetails.vue b/dashboard/src2/components/billing/PaymentDetails.vue index 1253cd8a5b..d054ff459f 100644 --- a/dashboard/src2/components/billing/PaymentDetails.vue +++ b/dashboard/src2/components/billing/PaymentDetails.vue @@ -1,15 +1,13 @@ diff --git a/press/api/partner.py b/press/api/partner.py index d0446e3a62..b04c354749 100644 --- a/press/api/partner.py +++ b/press/api/partner.py @@ -57,6 +57,7 @@ def get_partner_details(partner_email): "partner_type", "company_name", "custom_ongoing_period_fc_invoice_contribution", + "custom_fc_invoice_contribution", "partner_name", "custom_number_of_certified_members", "end_date", @@ -119,7 +120,7 @@ def transfer_credits(amount, customer, partner): @frappe.whitelist() -def get_partner_contribution(partner_email): +def get_partner_contribution_list(partner_email): partner_currency = frappe.db.get_value( "Team", {"erpnext_partner": 1, "partner_email": partner_email}, "currency" ) diff --git a/press/press/doctype/team/team.py b/press/press/doctype/team/team.py index 5900d71aa5..b3b1e34ec0 100644 --- a/press/press/doctype/team/team.py +++ b/press/press/doctype/team/team.py @@ -1416,7 +1416,7 @@ def enqueue_finalize_unpaid_for_team(team: str): doc.finalize_invoice() -def procees_partnership_fee(payment_intent): +def process_partnership_fee(payment_intent): from datetime import datetime if isinstance(payment_intent, str): @@ -1439,7 +1439,7 @@ def procees_partnership_fee(payment_intent): invoice = frappe.get_doc( doctype="Invoice", team=team.name, - type="Partnership Fee", + type="Partnership Fees", status="Paid", due_date=datetime.fromtimestamp(payment_intent["created"]), total=amount, From 9aa0fddb43632919c3aad0991824a064997b3525 Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Sun, 5 Jan 2025 19:23:13 +0530 Subject: [PATCH 05/12] feat(partner): Show Certified partner members list on dashboard --- .../components/partners/PartnerMembers.vue | 49 +++++++++++++++++++ .../components/partners/PartnerOverview.vue | 14 +++++- press/api/partner.py | 12 +++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 dashboard/src2/components/partners/PartnerMembers.vue diff --git a/dashboard/src2/components/partners/PartnerMembers.vue b/dashboard/src2/components/partners/PartnerMembers.vue new file mode 100644 index 0000000000..e7f72a677b --- /dev/null +++ b/dashboard/src2/components/partners/PartnerMembers.vue @@ -0,0 +1,49 @@ + + diff --git a/dashboard/src2/components/partners/PartnerOverview.vue b/dashboard/src2/components/partners/PartnerOverview.vue index d497f5f565..27467699bb 100644 --- a/dashboard/src2/components/partners/PartnerOverview.vue +++ b/dashboard/src2/components/partners/PartnerOverview.vue @@ -67,7 +67,7 @@
Certified Members
-
@@ -145,6 +145,16 @@ /> + + + +
@@ -155,11 +165,13 @@ import { FeatherIcon, Button, createResource, Progress } from 'frappe-ui'; import PartnerContribution from './PartnerContribution.vue'; import ClickToCopyField from '../ClickToCopyField.vue'; import PartnerCreditsForm from './PartnerCreditsForm.vue'; +import PartnerMembers from './PartnerMembers.vue'; const team = inject('team'); const showPartnerContributionDialog = ref(false); const showPartnerCreditsDialog = ref(false); +const showPartnerMembersDialog = ref(false); const partnerDetails = createResource({ url: 'press.api.partner.get_partner_details', diff --git a/press/api/partner.py b/press/api/partner.py index b04c354749..cbbec22369 100644 --- a/press/api/partner.py +++ b/press/api/partner.py @@ -187,6 +187,18 @@ def get_partner_customers(): return customers # noqa: RET504 +@frappe.whitelist() +def get_partner_members(partner): + from press.utils.billing import get_frappe_io_connection + + client = get_frappe_io_connection() + return client.get_list( + "LMS Certificate", + filters={"partner": partner}, + fields=["member_name", "member_email"], + ) + + @frappe.whitelist() def remove_partner(): team = get_current_team(get_doc=True) From 21588adbf47bb1e48b6a95190ce1115b57d11e7d Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Sun, 5 Jan 2025 19:36:28 +0530 Subject: [PATCH 06/12] fix(partners): Set empty list before request completion --- dashboard/src2/components/partners/PartnerMembers.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src2/components/partners/PartnerMembers.vue b/dashboard/src2/components/partners/PartnerMembers.vue index e7f72a677b..a9c546d6e6 100644 --- a/dashboard/src2/components/partners/PartnerMembers.vue +++ b/dashboard/src2/components/partners/PartnerMembers.vue @@ -32,7 +32,7 @@ const partnerMembers = createResource({ const partnerMembersList = computed(() => { return { - data: partnerMembers.data, + data: partnerMembers.data || [], selectable: false, columns: [ { From 6c065f7bf60b43cde9f8794584e798dcbeeb2afd Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Mon, 6 Jan 2025 13:28:53 +0530 Subject: [PATCH 07/12] feat(partner): Add type in Razorpay flow for partnership fee --- .../components/billing/BuyCreditsRazorpay.vue | 12 +++- .../partners/PartnerCreditsForm.vue | 6 +- .../components/partners/PartnerOverview.vue | 27 ++++----- press/api/billing.py | 6 +- .../razorpay_payment_record.json | 11 +++- .../razorpay_payment_record.py | 55 ++++++++++++++++++- 6 files changed, 96 insertions(+), 21 deletions(-) diff --git a/dashboard/src2/components/billing/BuyCreditsRazorpay.vue b/dashboard/src2/components/billing/BuyCreditsRazorpay.vue index 0f23c0f86b..4bb025f9f2 100644 --- a/dashboard/src2/components/billing/BuyCreditsRazorpay.vue +++ b/dashboard/src2/components/billing/BuyCreditsRazorpay.vue @@ -47,6 +47,10 @@ const props = defineProps({ minimumAmount: { type: Number, default: 0 + }, + type: { + type: String, + default: 'prepaid-credits' } }); @@ -72,9 +76,15 @@ onBeforeUnmount(() => { razorpayCheckoutJS.value?.remove(); }); +let order_type = + props.type === 'prepaid-credits' ? 'Prepaid Credits' : 'Partnership Fee'; + const createRazorpayOrder = createResource({ url: 'press.api.billing.create_razorpay_order', - params: { amount: props.amount }, + params: { + amount: props.amount, + type: order_type + }, onSuccess: data => processOrder(data), validate: () => { if (props.amount < props.minimumAmount) { diff --git a/dashboard/src2/components/partners/PartnerCreditsForm.vue b/dashboard/src2/components/partners/PartnerCreditsForm.vue index 894c0bfba2..7d8773001e 100644 --- a/dashboard/src2/components/partners/PartnerCreditsForm.vue +++ b/dashboard/src2/components/partners/PartnerCreditsForm.vue @@ -71,10 +71,11 @@ @cancel="show = false" /> - @@ -82,7 +83,7 @@ diff --git a/dashboard/src2/components/partners/BuyPartnerCreditsStripe.vue b/dashboard/src2/components/partners/BuyPartnerCreditsStripe.vue index fc0bb94bbe..3e00c18cfa 100644 --- a/dashboard/src2/components/partners/BuyPartnerCreditsStripe.vue +++ b/dashboard/src2/components/partners/BuyPartnerCreditsStripe.vue @@ -55,7 +55,7 @@ const props = defineProps({ type: Number, default: 0 }, - minimumAmount: { + maximumAmount: { type: Number, default: 0 } @@ -82,9 +82,9 @@ const createPaymentIntent = createResource({ url: 'press.api.billing.create_payment_intent_for_partnership_fees', params: { amount: props.amount }, validate() { - if (props.amount < props.minimumAmount && !team.doc.erpnext_partner) { + if (props.amount > props.maximumAmount && !team.doc.erpnext_partner) { throw new DashboardError( - `Amount must be greater than or equal to ${props.minimumAmount}` + `Amount must be lesser than or equal to ${props.maximumAmount}` ); } }, diff --git a/dashboard/src2/components/partners/PartnerCreditsForm.vue b/dashboard/src2/components/partners/PartnerCreditsForm.vue index 7d8773001e..e249037cda 100644 --- a/dashboard/src2/components/partners/PartnerCreditsForm.vue +++ b/dashboard/src2/components/partners/PartnerCreditsForm.vue @@ -66,15 +66,15 @@ -