Spaces:
Paused
Paused
Maksymilian Jankowski commited on
Commit Β·
5fed607
1
Parent(s): 3858e2a
update physical orders: colors
Browse files- routers/payments.py +57 -46
routers/payments.py
CHANGED
|
@@ -38,15 +38,16 @@ class UpdateSubscriptionRequest(BaseModel):
|
|
| 38 |
class PhysicalProductRequest(BaseModel):
|
| 39 |
generated_model_id: int
|
| 40 |
size_scale: float = 1.0
|
| 41 |
-
|
| 42 |
|
| 43 |
class PriceQuote(BaseModel):
|
| 44 |
material: str
|
|
|
|
| 45 |
mass_g: float
|
| 46 |
time_min: int
|
| 47 |
amount_gbp: float
|
| 48 |
client_secret: str
|
| 49 |
-
print_job_id: int
|
| 50 |
|
| 51 |
# Subscription plan configurations
|
| 52 |
SUBSCRIPTION_PLANS = {
|
|
@@ -199,11 +200,12 @@ async def quote_and_pay_physical_product(
|
|
| 199 |
"""
|
| 200 |
Get a price quote for 3D printing a generated model and create payment intent.
|
| 201 |
Retrieves the model from database and applies size scaling to calculate costs.
|
|
|
|
| 202 |
"""
|
| 203 |
try:
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
if request.size_scale <= 0:
|
| 208 |
raise HTTPException(status_code=400, detail="Size scale must be positive")
|
| 209 |
|
|
@@ -311,11 +313,11 @@ async def quote_and_pay_physical_product(
|
|
| 311 |
def estimate_fullness(vol_cm3: float) -> float:
|
| 312 |
"""Return a heuristic fullness (0β1) based on total volume."""
|
| 313 |
if vol_cm3 <= 500:
|
| 314 |
-
return
|
| 315 |
elif vol_cm3 <= 1500:
|
| 316 |
-
return
|
| 317 |
elif vol_cm3 <= 3000:
|
| 318 |
-
return 0.
|
| 319 |
else:
|
| 320 |
return 0.3 # large parts are mostly hollow / sparse infill
|
| 321 |
|
|
@@ -328,7 +330,7 @@ async def quote_and_pay_physical_product(
|
|
| 328 |
fullness_factor = estimate_fullness(bbox_volume_cm3)
|
| 329 |
scaled_volume_cm3 = bbox_volume_cm3 * fullness_factor
|
| 330 |
|
| 331 |
-
density = MATERIALS[
|
| 332 |
mass_g = scaled_volume_cm3 * density
|
| 333 |
|
| 334 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -349,7 +351,7 @@ async def quote_and_pay_physical_product(
|
|
| 349 |
# β 0.2 mm layer height
|
| 350 |
# β Deposition rate = nozzle Γ layer_height Γ print_speed (mmΒ³/s)
|
| 351 |
|
| 352 |
-
print_speed = MATERIALS[
|
| 353 |
nozzle_diameter_mm = 0.4
|
| 354 |
layer_height_mm = 0.2
|
| 355 |
deposition_area_mm2 = nozzle_diameter_mm * layer_height_mm # cross-sectional area of extrusion
|
|
@@ -371,7 +373,7 @@ async def quote_and_pay_physical_product(
|
|
| 371 |
)
|
| 372 |
|
| 373 |
# 4οΈβ£ Calculate pricing (server-authoritative)
|
| 374 |
-
material_price = mass_g * MATERIALS[
|
| 375 |
|
| 376 |
# Pricing model: material cost + setup fee, with a minimum total price.
|
| 377 |
MIN_TOTAL_PRICE_GBP = 2.00
|
|
@@ -394,7 +396,8 @@ async def quote_and_pay_physical_product(
|
|
| 394 |
metadata={
|
| 395 |
"user_id": current_user.id,
|
| 396 |
"generated_model_id": request.generated_model_id,
|
| 397 |
-
"material":
|
|
|
|
| 398 |
"size_scale": str(request.size_scale),
|
| 399 |
"mass_g": f"{mass_g:.2f}",
|
| 400 |
"time_min": str(time_min),
|
|
@@ -404,30 +407,14 @@ async def quote_and_pay_physical_product(
|
|
| 404 |
automatic_payment_methods={"enabled": True},
|
| 405 |
)
|
| 406 |
|
| 407 |
-
# 6οΈβ£ Store preliminary print job record
|
| 408 |
-
print_job_result = supabase.from_("Print_Jobs").insert({
|
| 409 |
-
"user_id": current_user.id,
|
| 410 |
-
"generated_model_id": request.generated_model_id,
|
| 411 |
-
"stripe_payment_intent_id": intent.id,
|
| 412 |
-
"status": "awaiting_payment",
|
| 413 |
-
"material": request.material,
|
| 414 |
-
"size_scale": request.size_scale,
|
| 415 |
-
"mass_g": mass_g,
|
| 416 |
-
"time_min": time_min,
|
| 417 |
-
"price_gbp": amount_gbp,
|
| 418 |
-
"model_name": model_data.get("model_name", ""),
|
| 419 |
-
"created_at": datetime.now().isoformat()
|
| 420 |
-
}).execute()
|
| 421 |
-
|
| 422 |
-
print_job_id = print_job_result.data[0]["id"] if print_job_result.data else None
|
| 423 |
-
|
| 424 |
return PriceQuote(
|
| 425 |
-
material=
|
|
|
|
| 426 |
mass_g=round(mass_g, 2),
|
| 427 |
time_min=time_min,
|
| 428 |
amount_gbp=amount_gbp,
|
| 429 |
client_secret=intent.client_secret,
|
| 430 |
-
print_job_id=
|
| 431 |
)
|
| 432 |
|
| 433 |
except HTTPException:
|
|
@@ -463,20 +450,30 @@ async def confirm_physical_payment(
|
|
| 463 |
|
| 464 |
# Check if this payment has already been processed
|
| 465 |
existing_job = supabase.from_("Print_Jobs").select("*").eq("stripe_payment_intent_id", request.payment_intent_id).execute()
|
| 466 |
-
if existing_job.data
|
| 467 |
return {"message": "Payment already processed", "print_job_id": existing_job.data[0]["id"]}
|
| 468 |
|
| 469 |
-
#
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
| 471 |
"status": "paid",
|
| 472 |
-
"
|
| 473 |
-
"
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
-
if not
|
| 477 |
-
raise HTTPException(status_code=
|
| 478 |
|
| 479 |
-
print_job =
|
| 480 |
|
| 481 |
# Record in Model_Order_History for tracking
|
| 482 |
supabase.from_("Model_Order_History").insert({
|
|
@@ -844,12 +841,26 @@ async def handle_physical_payment_succeeded(payment_intent):
|
|
| 844 |
return
|
| 845 |
|
| 846 |
try:
|
| 847 |
-
#
|
| 848 |
-
supabase.from_("Print_Jobs").
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
|
| 854 |
# Update Model_Order_History if exists
|
| 855 |
supabase.from_("Model_Order_History").update({
|
|
|
|
| 38 |
class PhysicalProductRequest(BaseModel):
|
| 39 |
generated_model_id: int
|
| 40 |
size_scale: float = 1.0
|
| 41 |
+
color: str = "gray"
|
| 42 |
|
| 43 |
class PriceQuote(BaseModel):
|
| 44 |
material: str
|
| 45 |
+
color: str
|
| 46 |
mass_g: float
|
| 47 |
time_min: int
|
| 48 |
amount_gbp: float
|
| 49 |
client_secret: str
|
| 50 |
+
print_job_id: Optional[int] = None
|
| 51 |
|
| 52 |
# Subscription plan configurations
|
| 53 |
SUBSCRIPTION_PLANS = {
|
|
|
|
| 200 |
"""
|
| 201 |
Get a price quote for 3D printing a generated model and create payment intent.
|
| 202 |
Retrieves the model from database and applies size scaling to calculate costs.
|
| 203 |
+
Material is always PLA, but user can select color.
|
| 204 |
"""
|
| 205 |
try:
|
| 206 |
+
# Material is always PLA
|
| 207 |
+
material = "pla"
|
| 208 |
+
|
| 209 |
if request.size_scale <= 0:
|
| 210 |
raise HTTPException(status_code=400, detail="Size scale must be positive")
|
| 211 |
|
|
|
|
| 313 |
def estimate_fullness(vol_cm3: float) -> float:
|
| 314 |
"""Return a heuristic fullness (0β1) based on total volume."""
|
| 315 |
if vol_cm3 <= 500:
|
| 316 |
+
return 1.5 # small parts tend to be more solid
|
| 317 |
elif vol_cm3 <= 1500:
|
| 318 |
+
return 1.2
|
| 319 |
elif vol_cm3 <= 3000:
|
| 320 |
+
return 0.6
|
| 321 |
else:
|
| 322 |
return 0.3 # large parts are mostly hollow / sparse infill
|
| 323 |
|
|
|
|
| 330 |
fullness_factor = estimate_fullness(bbox_volume_cm3)
|
| 331 |
scaled_volume_cm3 = bbox_volume_cm3 * fullness_factor
|
| 332 |
|
| 333 |
+
density = MATERIALS[material]["density"]
|
| 334 |
mass_g = scaled_volume_cm3 * density
|
| 335 |
|
| 336 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 351 |
# β 0.2 mm layer height
|
| 352 |
# β Deposition rate = nozzle Γ layer_height Γ print_speed (mmΒ³/s)
|
| 353 |
|
| 354 |
+
print_speed = MATERIALS[material]["speed_mm_s"]
|
| 355 |
nozzle_diameter_mm = 0.4
|
| 356 |
layer_height_mm = 0.2
|
| 357 |
deposition_area_mm2 = nozzle_diameter_mm * layer_height_mm # cross-sectional area of extrusion
|
|
|
|
| 373 |
)
|
| 374 |
|
| 375 |
# 4οΈβ£ Calculate pricing (server-authoritative)
|
| 376 |
+
material_price = mass_g * MATERIALS[material]["ppg"]
|
| 377 |
|
| 378 |
# Pricing model: material cost + setup fee, with a minimum total price.
|
| 379 |
MIN_TOTAL_PRICE_GBP = 2.00
|
|
|
|
| 396 |
metadata={
|
| 397 |
"user_id": current_user.id,
|
| 398 |
"generated_model_id": request.generated_model_id,
|
| 399 |
+
"material": material,
|
| 400 |
+
"color": request.color,
|
| 401 |
"size_scale": str(request.size_scale),
|
| 402 |
"mass_g": f"{mass_g:.2f}",
|
| 403 |
"time_min": str(time_min),
|
|
|
|
| 407 |
automatic_payment_methods={"enabled": True},
|
| 408 |
)
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
return PriceQuote(
|
| 411 |
+
material=material,
|
| 412 |
+
color=request.color,
|
| 413 |
mass_g=round(mass_g, 2),
|
| 414 |
time_min=time_min,
|
| 415 |
amount_gbp=amount_gbp,
|
| 416 |
client_secret=intent.client_secret,
|
| 417 |
+
print_job_id=None # Will be created only when payment is confirmed
|
| 418 |
)
|
| 419 |
|
| 420 |
except HTTPException:
|
|
|
|
| 450 |
|
| 451 |
# Check if this payment has already been processed
|
| 452 |
existing_job = supabase.from_("Print_Jobs").select("*").eq("stripe_payment_intent_id", request.payment_intent_id).execute()
|
| 453 |
+
if existing_job.data:
|
| 454 |
return {"message": "Payment already processed", "print_job_id": existing_job.data[0]["id"]}
|
| 455 |
|
| 456 |
+
# Create print job record now that payment is confirmed
|
| 457 |
+
print_job_result = supabase.from_("Print_Jobs").insert({
|
| 458 |
+
"user_id": current_user.id,
|
| 459 |
+
"generated_model_id": intent.metadata.get("generated_model_id"),
|
| 460 |
+
"stripe_payment_intent_id": request.payment_intent_id,
|
| 461 |
"status": "paid",
|
| 462 |
+
"material": intent.metadata.get("material"),
|
| 463 |
+
"color": intent.metadata.get("color"),
|
| 464 |
+
"size_scale": float(intent.metadata.get("size_scale")),
|
| 465 |
+
"mass_g": float(intent.metadata.get("mass_g")),
|
| 466 |
+
"time_min": int(intent.metadata.get("time_min")),
|
| 467 |
+
"price_gbp": float(intent.amount) / 100, # Convert from pence to pounds
|
| 468 |
+
"model_name": intent.metadata.get("model_name", ""),
|
| 469 |
+
"created_at": datetime.now().isoformat(),
|
| 470 |
+
"payment_confirmed_at": datetime.now().isoformat()
|
| 471 |
+
}).execute()
|
| 472 |
|
| 473 |
+
if not print_job_result.data:
|
| 474 |
+
raise HTTPException(status_code=500, detail="Failed to create print job record")
|
| 475 |
|
| 476 |
+
print_job = print_job_result.data[0]
|
| 477 |
|
| 478 |
# Record in Model_Order_History for tracking
|
| 479 |
supabase.from_("Model_Order_History").insert({
|
|
|
|
| 841 |
return
|
| 842 |
|
| 843 |
try:
|
| 844 |
+
# Check if print job already exists (from confirm_physical_payment endpoint)
|
| 845 |
+
existing_job = supabase.from_("Print_Jobs").select("*").eq("stripe_payment_intent_id", payment_intent["id"]).execute()
|
| 846 |
+
|
| 847 |
+
if not existing_job.data:
|
| 848 |
+
# Create print job record if it doesn't exist (webhook processed before API call)
|
| 849 |
+
supabase.from_("Print_Jobs").insert({
|
| 850 |
+
"user_id": user_id,
|
| 851 |
+
"generated_model_id": generated_model_id,
|
| 852 |
+
"stripe_payment_intent_id": payment_intent["id"],
|
| 853 |
+
"status": "paid",
|
| 854 |
+
"material": payment_intent.metadata.get("material"),
|
| 855 |
+
"color": payment_intent.metadata.get("color"),
|
| 856 |
+
"size_scale": float(payment_intent.metadata.get("size_scale")),
|
| 857 |
+
"mass_g": float(payment_intent.metadata.get("mass_g")),
|
| 858 |
+
"time_min": int(payment_intent.metadata.get("time_min")),
|
| 859 |
+
"price_gbp": float(payment_intent["amount"]) / 100,
|
| 860 |
+
"model_name": payment_intent.metadata.get("model_name", ""),
|
| 861 |
+
"created_at": datetime.now().isoformat(),
|
| 862 |
+
"payment_confirmed_at": datetime.now().isoformat()
|
| 863 |
+
}).execute()
|
| 864 |
|
| 865 |
# Update Model_Order_History if exists
|
| 866 |
supabase.from_("Model_Order_History").update({
|