Command: python -c "import base64;exec(base64.b64decode('ZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCnA9UGF0aChyJ0M6XFVzZXJzXGFzaFxEb2N1bWVudHNcU2hvcGVlQXBpXGFwcFxtYWluLnB5JykKbGluZXM9cC5yZWFkX3RleHQoZW5jb2Rpbmc9J3V0Zi04JyxlcnJvcnM9J3JlcGxhY2UnKS5zcGxpdGxpbmVzKCkKaGl0cz1baSBmb3IgaSx4IGluIGVudW1lcmF0ZShsaW5lcykgaWYgJy9hcGkvb3BlcmF0aW9ucy8nIGluIHggb3IgJ3JlcXVpcmVfb3BlcmF0aW9uc19kZXZpY2UnIGluIHggb3IgJ2lzX3ByaW1hcnknIGluIHhdCnNlZW49c2V0KCkKZm9yIGkgaW4gaGl0czoKICAgIGE9bWF4KDAsaS0xNSk7IGI9bWluKGxlbihsaW5lcyksaSs1NSkKICAgIGlmIGFueShhPj14IGFuZCBiPD15IGZvciB4LHkgaW4gc2Vlbik6IGNvbnRpbnVlCiAgICBzZWVuLmFkZCgoYSxiKSkKICAgIHByaW50KGYnLS0tIHthKzF9LXtifSAtLS0nKQogICAgZm9yIGogaW4gcmFuZ2UoYSxiKTogcHJpbnQoZid7aisxfToge2xpbmVzW2pdWzo1MDBdfScpCg=='))"
Directory: projects
Status: SUCCESS
Exit code: 0
Cancel Job Rerun Command Refresh
--- 222-291 ---
222: if x_device_token:
223: token_hash = hashlib.sha256(x_device_token.encode("utf-8")).hexdigest()
224: selected_shop = int(x_shop_id or settings.shop_id or 0)
225: device = db.authenticate_scanner_device(token_hash, selected_shop)
226: if not device:
227: device = db.authenticate_operations_device(token_hash, selected_shop)
228: if device:
229: return
230: if settings.admin_token and x_admin_token and hmac.compare_digest(x_admin_token, settings.admin_token):
231: return
232: if _read_session_cookie(request.cookies.get(SESSION_COOKIE)):
233: return
234: raise HTTPException(401, "Credential scanner tidak sah")
235:
236:
237: def require_operations_device(
238: x_device_token: str = Header(default="", alias="X-Device-Token"),
239: x_shop_id: int | None = Header(default=None, alias="X-Shop-Id"),
240: ) -> dict[str, Any]:
241: shop_id = int(x_shop_id or 0)
242: if not x_device_token or shop_id <= 0:
243: raise HTTPException(401, "Credential Shopee Operations tidak lengkap")
244: device = db.authenticate_operations_device(hashlib.sha256(x_device_token.encode("utf-8")).hexdigest(), shop_id)
245: if not device:
246: raise HTTPException(401, "Device Shopee Operations tidak sah")
247: return device
248:
249: class OrderSyncRequest(BaseModel):
250: from_date: date
251: to_date: date
252: include_details: bool = True
253: include_escrow: bool = True
254: include_logistics_preview: bool = False
255:
256: class ProductSyncRequest(BaseModel):
257: statuses: list[str] = Field(default_factory=lambda: ["NORMAL","UNLIST","BANNED"])
258:
259:
260: class ProductCostRequest(BaseModel):
261: item_id: int = Field(gt=0)
262: size_ml: Literal[10, 35]
263: unit_cost: float = Field(ge=0)
264: effective_from: date
265: currency: str = Field(default="MYR", min_length=3, max_length=3)
266:
267:
268: class UniversalCostRequest(BaseModel):
269: cost_10ml: float = Field(ge=0)
270: cost_35ml: float = Field(ge=0)
271: effective_from: date
272: currency: str = Field(default="MYR", min_length=3, max_length=3)
273:
274:
275: class FinanceSyncRequest(BaseModel):
276: lookback_days: int = Field(default=45, ge=1, le=3650)
277:
278:
279: class LlmContextRequest(BaseModel):
280: query: str = Field(min_length=1, max_length=2000)
281: product_hint: str = Field(default="", max_length=500)
282: order_sn: str = Field(default="", max_length=80)
283: limit: int = Field(default=5, ge=1, le=5)
284:
285: class ShopUpsertRequest(BaseModel):
286: shop_id: int
287: name: str = ""
288:
289:
290: class PerfumeRegistryRequest(BaseModel):
291: id: int | None = None
--- 919-988 ---
919:
920: def _operations_apk_storage_path(version_name: str) -> Path:
921: safe = re.sub(r"[^A-Za-z0-9._-]", "_", version_name)
922: return settings.data_dir / "downloads" / f"ShopeeOperations-{safe}.apk"
923:
924:
925: def _load_operations_version() -> dict[str, Any]:
926: if not _OPERATIONS_VERSION_FILE.is_file():
927: return {"version_code": 0, "version_name": "", "apk_size": 0, "apk_sha256": "", "changelog": "", "updated_at": ""}
928: try:
929: return json.loads(_OPERATIONS_VERSION_FILE.read_text(encoding="utf-8"))
930: except Exception:
931: return {"version_code": 0, "version_name": "", "apk_size": 0, "apk_sha256": "", "changelog": "", "updated_at": ""}
932:
933:
934: @app.post("/api/operations/app-upload", dependencies=[Depends(require_admin)])
935: async def api_operations_app_upload(
936: apk: UploadFile = File(...), version_name: str = Form(...), version_code: int = Form(...), changelog: str = Form(default=""),
937: ):
938: content = await apk.read()
939: if not content:
940: raise HTTPException(422, "APK kosong")
941: path = _operations_apk_storage_path(version_name)
942: path.parent.mkdir(parents=True, exist_ok=True)
943: path.write_bytes(content)
944: version = {
945: "version_code": int(version_code), "version_name": version_name, "apk_size": len(content),
946: "apk_sha256": hashlib.sha256(content).hexdigest(), "changelog": changelog,
947: "updated_at": datetime.now(safe_zoneinfo(settings.timezone)).isoformat(),
948: }
949: _OPERATIONS_VERSION_FILE.write_text(json.dumps(version, indent=2, ensure_ascii=False), encoding="utf-8")
950: for old in settings.data_dir.glob("downloads/ShopeeOperations-*.apk"):
951: if old.resolve() != path.resolve():
952: old.unlink(missing_ok=True)
953: return {"ok": True, **version}
954:
955:
956: @app.get("/api/operations/app-version")
957: def api_operations_app_version():
958: return {"ok": True, "app": _load_operations_version()}
959:
960:
961: @app.get("/downloads/ShopeeOperations.apk")
962: def operations_apk_download_latest():
963: version = _load_operations_version()
964: name = str(version.get("version_name") or "")
965: path = _operations_apk_storage_path(name)
966: if not name or not path.is_file():
967: raise HTTPException(404, "APK Shopee Operations belum dimuat naik")
968: return RedirectResponse(f"/downloads/ShopeeOperations-{name}.apk", headers={"Cache-Control": "no-store"})
969:
970:
971: @app.get("/downloads/ShopeeOperations-{version_name}.apk")
972: def operations_apk_download_versioned(version_name: str):
973: version = _load_operations_version()
974: if str(version.get("version_name") or "") != version_name:
975: raise HTTPException(404, "Versi APK Shopee Operations tidak dijumpai")
976: path = _operations_apk_storage_path(version_name)
977: if not path.is_file():
978: raise HTTPException(404, "Fail APK Shopee Operations tidak dijumpai")
979: return FileResponse(path, media_type="application/vnd.android.package-archive", filename=f"ShopeeOperations-{version_name}.apk",
980: headers={"Cache-Control": "public, max-age=86400", "X-App-Version-Code": str(version.get("version_code", 0))})
981:
982:
983: @app.post("/api/scanner/app-upload", dependencies=[Depends(require_admin)])
984: async def api_scanner_app_upload(
985: apk: UploadFile = File(...),
986: version_name: str = Form(...),
987: version_code: int = Form(...),
988: changelog: str = Form(default=""),
--- 941-1010 ---
941: path = _operations_apk_storage_path(version_name)
942: path.parent.mkdir(parents=True, exist_ok=True)
943: path.write_bytes(content)
944: version = {
945: "version_code": int(version_code), "version_name": version_name, "apk_size": len(content),
946: "apk_sha256": hashlib.sha256(content).hexdigest(), "changelog": changelog,
947: "updated_at": datetime.now(safe_zoneinfo(settings.timezone)).isoformat(),
948: }
949: _OPERATIONS_VERSION_FILE.write_text(json.dumps(version, indent=2, ensure_ascii=False), encoding="utf-8")
950: for old in settings.data_dir.glob("downloads/ShopeeOperations-*.apk"):
951: if old.resolve() != path.resolve():
952: old.unlink(missing_ok=True)
953: return {"ok": True, **version}
954:
955:
956: @app.get("/api/operations/app-version")
957: def api_operations_app_version():
958: return {"ok": True, "app": _load_operations_version()}
959:
960:
961: @app.get("/downloads/ShopeeOperations.apk")
962: def operations_apk_download_latest():
963: version = _load_operations_version()
964: name = str(version.get("version_name") or "")
965: path = _operations_apk_storage_path(name)
966: if not name or not path.is_file():
967: raise HTTPException(404, "APK Shopee Operations belum dimuat naik")
968: return RedirectResponse(f"/downloads/ShopeeOperations-{name}.apk", headers={"Cache-Control": "no-store"})
969:
970:
971: @app.get("/downloads/ShopeeOperations-{version_name}.apk")
972: def operations_apk_download_versioned(version_name: str):
973: version = _load_operations_version()
974: if str(version.get("version_name") or "") != version_name:
975: raise HTTPException(404, "Versi APK Shopee Operations tidak dijumpai")
976: path = _operations_apk_storage_path(version_name)
977: if not path.is_file():
978: raise HTTPException(404, "Fail APK Shopee Operations tidak dijumpai")
979: return FileResponse(path, media_type="application/vnd.android.package-archive", filename=f"ShopeeOperations-{version_name}.apk",
980: headers={"Cache-Control": "public, max-age=86400", "X-App-Version-Code": str(version.get("version_code", 0))})
981:
982:
983: @app.post("/api/scanner/app-upload", dependencies=[Depends(require_admin)])
984: async def api_scanner_app_upload(
985: apk: UploadFile = File(...),
986: version_name: str = Form(...),
987: version_code: int = Form(...),
988: changelog: str = Form(default=""),
989: force_update: bool = Form(default=False),
990: ):
991: content = await apk.read()
992: if not content:
993: raise HTTPException(422, "APK kosong")
994: apk_sha256 = hashlib.sha256(content).hexdigest()
995: apk_path = _apk_storage_path(version_name)
996: apk_path.parent.mkdir(parents=True, exist_ok=True)
997: apk_path.write_bytes(content)
998: _tz = safe_zoneinfo(settings.timezone)
999: version = {
1000: "version_code": version_code,
1001: "version_name": version_name,
1002: "apk_size": len(content),
1003: "apk_sha256": apk_sha256,
1004: "changelog": changelog,
1005: "force_update": force_update,
1006: "updated_at": datetime.now(_tz).isoformat(),
1007: }
1008: _save_app_version(version)
1009: old_apks = sorted(settings.data_dir.glob("downloads/LabelScanStock-*.apk"))
1010: for old in old_apks:
--- 3158-3227 ---
3158: shop_id=shop_id,
3159: device_id=req.device_id,
3160: token_sha256=hashlib.sha256(device_token.encode("utf-8")).hexdigest(),
3161: device_name=req.device_name,
3162: app_version=req.app_version,
3163: )
3164: db.log_audit(
3165: actor=f"scanner:{req.device_id}",
3166: action="scanner_device_enrolled",
3167: target=req.device_id,
3168: details={"shop_id": shop_id, "app_version": req.app_version},
3169: )
3170: return {"ok": True, "shop_id": shop_id, "device_id": req.device_id, "device_token": device_token}
3171:
3172:
3173: @app.post("/api/operations/enroll", dependencies=[Depends(require_admin)])
3174: def api_operations_enroll(req: OperationsEnrollmentRequest):
3175: valid_shop_ids = {int(item.get("shop_id") or 0) for item in _shop_records()}
3176: if req.shop_id not in valid_shop_ids:
3177: raise HTTPException(404, "Shop belum didaftarkan pada server")
3178: device_token = secrets.token_urlsafe(32)
3179: device = db.enroll_operations_device(
3180: req.shop_id,
3181: req.device_id,
3182: hashlib.sha256(device_token.encode("utf-8")).hexdigest(),
3183: req.device_name,
3184: req.app_version,
3185: )
3186: db.log_audit(
3187: actor=f"operations:{req.device_id}", action="operations_device_enrolled",
3188: target=req.device_id, details={"shop_id": req.shop_id, "app_version": req.app_version},
3189: )
3190: return {"ok": True, "shop_id": req.shop_id, "device": device, "device_token": device_token}
3191:
3192:
3193: @app.get("/api/operations/status")
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
--- 3178-3247 ---
3178: device_token = secrets.token_urlsafe(32)
3179: device = db.enroll_operations_device(
3180: req.shop_id,
3181: req.device_id,
3182: hashlib.sha256(device_token.encode("utf-8")).hexdigest(),
3183: req.device_name,
3184: req.app_version,
3185: )
3186: db.log_audit(
3187: actor=f"operations:{req.device_id}", action="operations_device_enrolled",
3188: target=req.device_id, details={"shop_id": req.shop_id, "app_version": req.app_version},
3189: )
3190: return {"ok": True, "shop_id": req.shop_id, "device": device, "device_token": device_token}
3191:
3192:
3193: @app.get("/api/operations/status")
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
--- 3179-3248 ---
3179: device = db.enroll_operations_device(
3180: req.shop_id,
3181: req.device_id,
3182: hashlib.sha256(device_token.encode("utf-8")).hexdigest(),
3183: req.device_name,
3184: req.app_version,
3185: )
3186: db.log_audit(
3187: actor=f"operations:{req.device_id}", action="operations_device_enrolled",
3188: target=req.device_id, details={"shop_id": req.shop_id, "app_version": req.app_version},
3189: )
3190: return {"ok": True, "shop_id": req.shop_id, "device": device, "device_token": device_token}
3191:
3192:
3193: @app.get("/api/operations/status")
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
--- 3186-3255 ---
3186: db.log_audit(
3187: actor=f"operations:{req.device_id}", action="operations_device_enrolled",
3188: target=req.device_id, details={"shop_id": req.shop_id, "app_version": req.app_version},
3189: )
3190: return {"ok": True, "shop_id": req.shop_id, "device": device, "device_token": device_token}
3191:
3192:
3193: @app.get("/api/operations/status")
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
--- 3191-3260 ---
3191:
3192:
3193: @app.get("/api/operations/status")
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
--- 3194-3263 ---
3194: def api_operations_status(device: dict[str, Any] = Depends(require_operations_device)):
3195: shop_id = int(device["shop_id"])
3196: alert_settings = db.operations_alert_settings(shop_id)
3197: return {
3198: "ok": True,
3199: "shop_id": shop_id,
3200: "device_id": device["device_id"],
3201: "is_primary": str(alert_settings.get("primary_device_id") or "") == str(device["device_id"]),
3202: "alert_settings": alert_settings,
3203: }
3204:
3205:
3206: @app.put("/api/operations/primary-device")
3207: def api_operations_primary_device(
3208: req: OperationsPrimaryRequest,
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
--- 3209-3278 ---
3209: device: dict[str, Any] = Depends(require_operations_device),
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
--- 3210-3279 ---
3210: ):
3211: if not req.confirmed:
3212: raise HTTPException(409, "Confirmation diperlukan untuk menukar telefon utama")
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
--- 3213-3282 ---
3213: try:
3214: alert_settings = db.set_operations_primary_device(int(device["shop_id"]), str(device["device_id"]))
3215: except ValueError as exc:
3216: raise HTTPException(422, str(exc)) from exc
3217: db.log_audit(
3218: actor=f"operations:{device['device_id']}", action="instant_alert_primary_changed",
3219: target=str(device["shop_id"]), details={"primary_device_id": device["device_id"]},
3220: )
3221: return {"ok": True, "alert_settings": alert_settings}
3222:
3223:
3224: @app.get("/api/operations/instant-delivery/feed")
3225: def api_operations_instant_delivery_feed(device: dict[str, Any] = Depends(require_operations_device)):
3226: shop_id = int(device["shop_id"])
3227: alert_settings = db.operations_alert_settings(shop_id)
3228: is_primary = str(alert_settings.get("primary_device_id") or "") == str(device["device_id"])
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
3280: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scents WHERE status='ACTIVE'"
3281: ).fetchone()
3282: canonical_alias = conn.execute(
--- 3229-3298 ---
3229: overview = db.get_ready_to_ship_overview()
3230: orders = overview.get("instant_delivery_orders") or []
3231: acknowledgements = db.instant_delivery_acknowledgements(shop_id, [str(row.get("order_sn") or "") for row in orders])
3232: payload_orders = []
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
3280: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scents WHERE status='ACTIVE'"
3281: ).fetchone()
3282: canonical_alias = conn.execute(
3283: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scent_aliases"
3284: ).fetchone()
3285: h.update(
3286: f"canonical:{canonical[0]}:{canonical[1]}:{canonical_alias[0]}:{canonical_alias[1]}".encode()
3287: )
3288: for table, timestamp_column in (
3289: ("canonical_stickers", "updated_at"),
3290: ("products", "synced_at"),
3291: ("product_variations", "synced_at"),
3292: ("shop_product_mappings", "updated_at"),
3293: ):
3294: row = conn.execute(
3295: f"SELECT COALESCE(MAX({timestamp_column}),'0'),COUNT(*) FROM {table} WHERE shop_id=?",
3296: (shop_id,),
3297: ).fetchone()
3298: h.update(f"{table}:{row[0]}:{row[1]}".encode())
--- 3233-3302 ---
3233: for row in orders:
3234: item = dict(row)
3235: acknowledgement = acknowledgements.get(str(item.get("order_sn") or ""))
3236: item["acknowledged"] = bool(acknowledgement)
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
3280: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scents WHERE status='ACTIVE'"
3281: ).fetchone()
3282: canonical_alias = conn.execute(
3283: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scent_aliases"
3284: ).fetchone()
3285: h.update(
3286: f"canonical:{canonical[0]}:{canonical[1]}:{canonical_alias[0]}:{canonical_alias[1]}".encode()
3287: )
3288: for table, timestamp_column in (
3289: ("canonical_stickers", "updated_at"),
3290: ("products", "synced_at"),
3291: ("product_variations", "synced_at"),
3292: ("shop_product_mappings", "updated_at"),
3293: ):
3294: row = conn.execute(
3295: f"SELECT COALESCE(MAX({timestamp_column}),'0'),COUNT(*) FROM {table} WHERE shop_id=?",
3296: (shop_id,),
3297: ).fetchone()
3298: h.update(f"{table}:{row[0]}:{row[1]}".encode())
3299: for table in (
3300: "perfume_aliases",
3301: "canonical_products",
3302: "canonical_aliases",
--- 3237-3306 ---
3237: item["acknowledged_at"] = acknowledgement.get("acknowledged_at") if acknowledgement else None
3238: payload_orders.append(item)
3239: return {
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
3280: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scents WHERE status='ACTIVE'"
3281: ).fetchone()
3282: canonical_alias = conn.execute(
3283: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scent_aliases"
3284: ).fetchone()
3285: h.update(
3286: f"canonical:{canonical[0]}:{canonical[1]}:{canonical_alias[0]}:{canonical_alias[1]}".encode()
3287: )
3288: for table, timestamp_column in (
3289: ("canonical_stickers", "updated_at"),
3290: ("products", "synced_at"),
3291: ("product_variations", "synced_at"),
3292: ("shop_product_mappings", "updated_at"),
3293: ):
3294: row = conn.execute(
3295: f"SELECT COALESCE(MAX({timestamp_column}),'0'),COUNT(*) FROM {table} WHERE shop_id=?",
3296: (shop_id,),
3297: ).fetchone()
3298: h.update(f"{table}:{row[0]}:{row[1]}".encode())
3299: for table in (
3300: "perfume_aliases",
3301: "canonical_products",
3302: "canonical_aliases",
3303: "perfume_storage",
3304: ):
3305: row = conn.execute(
3306: f"SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM {table}"
--- 3240-3309 ---
3240: "ok": True,
3241: "server_time": datetime.now(timezone.utc).isoformat(),
3242: "shop_id": shop_id,
3243: "device_id": device["device_id"],
3244: "is_primary": is_primary,
3245: "poll_after_seconds": int(alert_settings.get("poll_seconds") or 30),
3246: "repeat_after_seconds": int(alert_settings.get("repeat_seconds") or 300),
3247: "count": len(payload_orders),
3248: "orders": payload_orders if is_primary else [],
3249: }
3250:
3251:
3252: @app.post("/api/operations/instant-delivery/ack")
3253: def api_operations_instant_delivery_ack(
3254: req: OperationsAckRequest,
3255: device: dict[str, Any] = Depends(require_operations_device),
3256: ):
3257: shop_id = int(device["shop_id"])
3258: alert_settings = db.operations_alert_settings(shop_id)
3259: if str(alert_settings.get("primary_device_id") or "") != str(device["device_id"]):
3260: raise HTTPException(403, "Hanya telefon utama boleh mengesahkan Instant Delivery")
3261: acknowledgement = db.acknowledge_instant_delivery(shop_id, req.order_sn, str(device["device_id"]))
3262: db.log_audit(
3263: actor=f"operations:{device['device_id']}", action="instant_delivery_ready_acknowledged",
3264: target=req.order_sn, details={"shop_id": shop_id},
3265: )
3266: return {"ok": True, "acknowledgement": acknowledgement}
3267:
3268:
3269: def _scanner_catalog_revision(shop_id: int) -> str:
3270: """Revision for registry/inventory entities copied to the phone."""
3271: import hashlib as _hl
3272: h = _hl.sha256()
3273: with db.connect() as conn:
3274: registry = conn.execute(
3275: "SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM perfume_name_registry WHERE shop_id IN (0, ?)",
3276: (shop_id,),
3277: ).fetchone()
3278: h.update(f"registry:{registry[0]}:{registry[1]}".encode())
3279: canonical = conn.execute(
3280: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scents WHERE status='ACTIVE'"
3281: ).fetchone()
3282: canonical_alias = conn.execute(
3283: "SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM canonical_scent_aliases"
3284: ).fetchone()
3285: h.update(
3286: f"canonical:{canonical[0]}:{canonical[1]}:{canonical_alias[0]}:{canonical_alias[1]}".encode()
3287: )
3288: for table, timestamp_column in (
3289: ("canonical_stickers", "updated_at"),
3290: ("products", "synced_at"),
3291: ("product_variations", "synced_at"),
3292: ("shop_product_mappings", "updated_at"),
3293: ):
3294: row = conn.execute(
3295: f"SELECT COALESCE(MAX({timestamp_column}),'0'),COUNT(*) FROM {table} WHERE shop_id=?",
3296: (shop_id,),
3297: ).fetchone()
3298: h.update(f"{table}:{row[0]}:{row[1]}".encode())
3299: for table in (
3300: "perfume_aliases",
3301: "canonical_products",
3302: "canonical_aliases",
3303: "perfume_storage",
3304: ):
3305: row = conn.execute(
3306: f"SELECT COALESCE(MAX(updated_at),'0'),COUNT(*) FROM {table}"
3307: ).fetchone()
3308: h.update(f"{table}:{row[0]}:{row[1]}".encode())
3309: for table in (