1515from paddle_billing .Resources .Prices .Operations import CreatePrice
1616from paddle_billing .Resources .Products .Operations import CreateProduct
1717
18- # -------------------------------------------------------------------
19- # Plan definitions
20- # -------------------------------------------------------------------
2118
2219@dataclass (frozen = True )
2320class PlanDefinition :
2421 key : str
2522 name : str
26- monthly_price_usd : str # MUST be string for Paddle Money.amount
23+ monthly_price_usd : str
2724 trial_days : int | None = None
2825
2926
@@ -43,22 +40,16 @@ class PlanDefinition:
4340)
4441
4542
46- # -------------------------------------------------------------------
47- # Public entrypoint
48- # -------------------------------------------------------------------
49-
5043def provision_paddle_plans () -> None :
51- """Idempotently provision Paddle products (plans) and monthly subscription prices.
52-
53- Rules:
54- - One Product per plan (Starter, Pro)
55- - One monthly subscription Price per Product
56- - Starter has a trial; Pro does not
57- - SDK-only (no raw HTTP)
58- """
44+ """Idempotently provision Paddle products (plans) and monthly subscription prices."""
5945 from langflow .services .paddle .client import get_paddle_client
46+
6047 client = get_paddle_client ()
6148
49+ if not _acquire_provisioning_lock (client ):
50+ logger .info ("Paddle provisioning already in progress or completed; skipping." )
51+ return
52+
6253 products = list (client .products .list ())
6354 prices = list (client .prices .list ())
6455
@@ -74,10 +65,6 @@ def provision_paddle_plans() -> None:
7465 logger .info ("Paddle plan %s provisioned." , plan .key )
7566
7667
77- # -------------------------------------------------------------------
78- # Find helpers
79- # -------------------------------------------------------------------
80-
8168def _find_existing_product (plan : PlanDefinition , products : list [Any ]) -> Any | None :
8269 for product in products :
8370 if _custom_data_value (product .custom_data , "plan_key" ) == plan .key :
@@ -87,14 +74,17 @@ def _find_existing_product(plan: PlanDefinition, products: list[Any]) -> Any | N
8774 return None
8875
8976
90- def _find_existing_price (plan : PlanDefinition , prices : list [Any ], product : Any | None ) -> Any | None :
77+ def _find_existing_price (
78+ plan : PlanDefinition , prices : list [Any ], product : Any | None
79+ ) -> Any | None :
9180 for price in prices :
9281 if _custom_data_value (price .custom_data , "plan_key" ) == plan .key :
9382 return price
9483 if product and _price_matches_plan (plan , price , product .id ):
9584 return price
9685 return None
9786
87+
9888def _price_matches_plan (plan : PlanDefinition , price : Any , product_id : str ) -> bool :
9989 if getattr (price , "product_id" , None ) != product_id :
10090 return False
@@ -121,22 +111,35 @@ def _duration_matches(duration: Any, interval: Interval, frequency: int) -> bool
121111 and getattr (duration , "frequency" , None ) == frequency
122112 )
123113
124- def _custom_data_value (custom_data : Any , key : str ) -> Any | None :
125- """Paddle CustomData is NOT a dict.
126114
127- Safe accessor that works across SDK versions.
128- """
115+ def _custom_data_value (custom_data : Any , key : str ) -> Any | None :
129116 if not custom_data :
130117 return None
131118 try :
132119 return custom_data [key ]
133- except Exception : # noqa: BLE001
120+ except Exception : # noqa: BLE001
134121 return None
135122
136123
137- # -------------------------------------------------------------------
138- # Create helpers
139- # -------------------------------------------------------------------
124+ def _acquire_provisioning_lock (client : Any ) -> bool :
125+ LOCK_KEY = "paddle_provisioning_lock"
126+
127+ for product in client .products .list ():
128+ if _custom_data_value (product .custom_data , "lock" ) == LOCK_KEY :
129+ return False
130+
131+ try :
132+ client .products .create (
133+ CreateProduct (
134+ name = "Paddle Provisioning Lock" ,
135+ tax_category = TaxCategory .Standard ,
136+ custom_data = {"lock" : LOCK_KEY },
137+ )
138+ )
139+ return True
140+ except Exception :
141+ return False
142+
140143
141144def _create_product (plan : PlanDefinition , client : Any ) -> str :
142145 logger .info ("Creating Paddle product for plan %s." , plan .key )
@@ -166,11 +169,11 @@ def _create_monthly_price(plan: PlanDefinition, product_id: str, client: Any) ->
166169 CreatePrice (
167170 product_id = product_id ,
168171 description = f"{ plan .name } Monthly" ,
169- billing_cycle = billing_cycle , # subscription
170- trial_period = trial_period , # Starter only
172+ billing_cycle = billing_cycle ,
173+ trial_period = trial_period ,
171174 tax_mode = TaxMode .External ,
172175 unit_price = Money (
173- amount = plan .monthly_price_usd , # MUST be string
176+ amount = plan .monthly_price_usd ,
174177 currency_code = CurrencyCode .USD ,
175178 ),
176179 custom_data = {
0 commit comments