Update main.py
Browse files
main.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
| 10 |
import base64
|
| 11 |
import hashlib
|
| 12 |
import hmac
|
|
|
|
| 13 |
|
| 14 |
app = FastAPI(title="Cié Cié Backend API")
|
| 15 |
|
|
@@ -38,6 +39,7 @@ RETURN_URL = "https://ciecietaipei.github.io/booking.html"
|
|
| 38 |
|
| 39 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
|
| 40 |
|
|
|
|
| 41 |
class OrderPayload(BaseModel):
|
| 42 |
service_type: str
|
| 43 |
name: str
|
|
@@ -55,6 +57,12 @@ class ConfirmPayload(BaseModel):
|
|
| 55 |
order_id: str
|
| 56 |
amount: int
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
@app.get("/")
|
| 59 |
def read_root():
|
| 60 |
return {"status": "online", "message": "Cié Cié FastAPI is running."}
|
|
@@ -86,7 +94,6 @@ async def submit_booking(payload: OrderPayload):
|
|
| 86 |
"products": [{"name": "餐飲訂金與預付金", "quantity": 1, "price": final_deposit}]
|
| 87 |
}],
|
| 88 |
"redirectUrls": {
|
| 89 |
-
# 這裡最關鍵:帶上 action=payment_confirm 讓前端知道要處理結帳確認
|
| 90 |
"confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={final_deposit}",
|
| 91 |
"cancelUrl": f"{RETURN_URL}?action=payment_cancel"
|
| 92 |
}
|
|
@@ -139,7 +146,7 @@ async def submit_booking(payload: OrderPayload):
|
|
| 139 |
return { "status": "success", "message": "訂位已成功建立!" }
|
| 140 |
except Exception as e: raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
|
| 141 |
|
| 142 |
-
#
|
| 143 |
@app.post("/api/linepay/confirm")
|
| 144 |
async def confirm_payment(payload: ConfirmPayload):
|
| 145 |
uri = f"/v3/payments/{payload.transaction_id}/confirm"
|
|
@@ -172,6 +179,66 @@ async def confirm_payment(payload: ConfirmPayload):
|
|
| 172 |
except Exception as e:
|
| 173 |
raise HTTPException(status_code=500, detail=str(e))
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
def notify_boss(name, tel, date, time, pax, amount):
|
| 176 |
if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID: return
|
| 177 |
msg = f"🔔 【新訂單通知】\n姓��:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位"
|
|
|
|
| 10 |
import base64
|
| 11 |
import hashlib
|
| 12 |
import hmac
|
| 13 |
+
import time
|
| 14 |
|
| 15 |
app = FastAPI(title="Cié Cié Backend API")
|
| 16 |
|
|
|
|
| 39 |
|
| 40 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
|
| 41 |
|
| 42 |
+
# --- 資料結構定義 ---
|
| 43 |
class OrderPayload(BaseModel):
|
| 44 |
service_type: str
|
| 45 |
name: str
|
|
|
|
| 57 |
order_id: str
|
| 58 |
amount: int
|
| 59 |
|
| 60 |
+
# 🌟 新增:用來接收補付款請求的資料結構
|
| 61 |
+
class RepayPayload(BaseModel):
|
| 62 |
+
order_id: str
|
| 63 |
+
|
| 64 |
+
# --- API 端點定義 ---
|
| 65 |
+
|
| 66 |
@app.get("/")
|
| 67 |
def read_root():
|
| 68 |
return {"status": "online", "message": "Cié Cié FastAPI is running."}
|
|
|
|
| 94 |
"products": [{"name": "餐飲訂金與預付金", "quantity": 1, "price": final_deposit}]
|
| 95 |
}],
|
| 96 |
"redirectUrls": {
|
|
|
|
| 97 |
"confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={final_deposit}",
|
| 98 |
"cancelUrl": f"{RETURN_URL}?action=payment_cancel"
|
| 99 |
}
|
|
|
|
| 146 |
return { "status": "success", "message": "訂位已成功建立!" }
|
| 147 |
except Exception as e: raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
|
| 148 |
|
| 149 |
+
# 確認收錢的端點 (Confirm API)
|
| 150 |
@app.post("/api/linepay/confirm")
|
| 151 |
async def confirm_payment(payload: ConfirmPayload):
|
| 152 |
uri = f"/v3/payments/{payload.transaction_id}/confirm"
|
|
|
|
| 179 |
except Exception as e:
|
| 180 |
raise HTTPException(status_code=500, detail=str(e))
|
| 181 |
|
| 182 |
+
# 🌟 新增:處理重新產生付款連結的 API 🌟
|
| 183 |
+
@app.post("/api/linepay/repay")
|
| 184 |
+
async def repay_payment(payload: RepayPayload):
|
| 185 |
+
if not supabase: raise HTTPException(status_code=500, detail="資料庫未連線")
|
| 186 |
+
|
| 187 |
+
try:
|
| 188 |
+
# 1. 找回原本的訂單
|
| 189 |
+
res = supabase.table("bookings").select("*").ilike("remarks", f"%{payload.order_id}%").execute()
|
| 190 |
+
if not res.data: raise HTTPException(status_code=404, detail="找不到該筆訂單")
|
| 191 |
+
|
| 192 |
+
booking = res.data[0]
|
| 193 |
+
# 防呆:如果客人其實已經付過錢了,阻止他重複刷卡
|
| 194 |
+
if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
|
| 195 |
+
raise HTTPException(status_code=400, detail="此訂單已完成付款或確認,無需重新結帳")
|
| 196 |
+
|
| 197 |
+
# 2. 獲取要收取的金額 (PoC 階段先以防護訂金的 1000 元作為補收預設值)
|
| 198 |
+
# 備註:在最終版的 orders 表格中,我們會直接從資料庫精準讀取 deposit_amount
|
| 199 |
+
amount = 1000
|
| 200 |
+
|
| 201 |
+
# 3. 重新向 LINE Pay 申請新的交易連結
|
| 202 |
+
# 技巧:在原本的 order_id 後面加上時間戳記,繞過 LINE Pay 「同訂單號不可重複請求」的限制
|
| 203 |
+
new_order_id = f"{payload.order_id}-R{int(time.time())}"
|
| 204 |
+
|
| 205 |
+
request_body = {
|
| 206 |
+
"amount": amount,
|
| 207 |
+
"currency": "TWD",
|
| 208 |
+
"orderId": new_order_id,
|
| 209 |
+
"packages": [{
|
| 210 |
+
"id": "pkg_repay", "amount": amount, "name": "Cié Cié Taipei 補繳訂金",
|
| 211 |
+
"products": [{"name": "餐飲訂金", "quantity": 1, "price": amount}]
|
| 212 |
+
}],
|
| 213 |
+
"redirectUrls": {
|
| 214 |
+
# 付款成功後,一樣帶回前端進行確認
|
| 215 |
+
"confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={amount}&orderId={payload.order_id}",
|
| 216 |
+
"cancelUrl": f"{RETURN_URL}?action=payment_cancel"
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
uri = "/v3/payments/request"
|
| 221 |
+
nonce = str(uuid.uuid4())
|
| 222 |
+
body_str = json.dumps(request_body)
|
| 223 |
+
message = LINE_PAY_CHANNEL_SECRET + uri + body_str + nonce
|
| 224 |
+
signature = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode()
|
| 225 |
+
|
| 226 |
+
headers = {
|
| 227 |
+
"Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
|
| 228 |
+
"X-LINE-Authorization-Nonce": nonce, "X-LINE-Authorization": signature
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
line_pay_res = requests.post(f"{LINE_PAY_BASE_URL}{uri}", headers=headers, data=body_str)
|
| 232 |
+
res_data = line_pay_res.json()
|
| 233 |
+
|
| 234 |
+
if res_data.get("returnCode") == "0000":
|
| 235 |
+
return {"payment_url": res_data["info"]["paymentUrl"]["web"]}
|
| 236 |
+
else:
|
| 237 |
+
raise HTTPException(status_code=500, detail=f"LINE Pay 錯誤: {res_data.get('returnMessage')}")
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 241 |
+
|
| 242 |
def notify_boss(name, tel, date, time, pax, amount):
|
| 243 |
if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID: return
|
| 244 |
msg = f"🔔 【新訂單通知】\n姓��:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位"
|