Command: python -c "from pathlib import Path; p=Path(r'C:\\Users\\ash\\Documents\\ShopeeApi\\app\\main.py'); lines=p.read_text(encoding='utf-8',errors='replace').splitlines(); hits=[i for i,x in enumerate(lines) if 'arrange-all-ready-to-ship' in x or 'instant-delivery/feed' in x or 'def _is_instant' in x.lower() or 'instant delivery' in x.lower()]; ranges=[]; [(ranges.append((max(0,i-45),min(len(lines),i+180)))) for i in hits]; seen=set(); [(print('---HIT',a+1,b,'---'), [print(f'{j+1}: {lines[j]}') for j in range(a,b) if j not in seen and not seen.add(j)]) for a,b in ranges]"
Directory: projects
Status: SUCCESS
Exit code: 0
Cancel Job Rerun Command Refresh
---HIT 2162 2386 ---
2162: package_number = str(item.get("package_number") or "").strip()
2163: if not order_sn:
2164: continue
2165: # Shopee requires package_number to be omitted for some unsplit orders,
2166: # while our local printable queue is keyed by the real package number.
2167: # Recover that local key without changing the package sent to Shopee.
2168: if not package_number:
2169: order = db.get_order(order_sn, shop_id) or {}
2170: local_packages = {
2171: str(package.get("package_number") or "").strip()
2172: for package in (order.get("packages") or [])
2173: if isinstance(package, dict) and str(package.get("package_number") or "").strip()
2174: }
2175: if len(local_packages) == 1:
2176: package_number = next(iter(local_packages))
2177: key = (order_sn, package_number)
2178: existing = unique.get(key) or {"order_sn": order_sn, "package_number": package_number, "tracking_number": ""}
2179: if item.get("tracking_number"):
2180: existing["tracking_number"] = str(item.get("tracking_number") or "").strip()
2181: if "api_package_number" in item:
2182: existing["api_package_number"] = str(item.get("api_package_number") or "").strip()
2183: unique[key] = existing
2184: payload = list(unique.values())
2185: if not payload:
2186: return None
2187: job_id = db.create_sync_job(
2188: shop_id,
2189: "awb_prepare",
2190: {"documents": payload, "force_download": False, "source": source},
2191: len(payload),
2192: actor="arrange_shipment",
2193: )
2194: started = start(job_id)
2195: if not started:
2196: db.update_job(job_id, status="failed", last_error="Worker awb_prepare selepas arrange shipment tidak dapat dimulakan.")
2197: db.log_audit("admin", "awb_prepare_after_arrange", job_id, {"shop_id": shop_id, "requested": len(payload), "started": started, "source": source})
2198: return {"job_id": job_id, "requested": len(payload), "started": bool(started)}
2199:
2200: @app.get("/api/shipment/{order_sn}/parameters", dependencies=[Depends(require_admin)])
2201: def shipping_parameters(order_sn:str, package_number:str|None=None):
2202: client=ShopeeClient()
2203: try: return client.get_shipping_parameter(order_sn,package_number)
2204: finally: client.close()
2205:
2206:
2207: @app.post("/api/fulfillment/arrange-all-ready-to-ship", dependencies=[Depends(require_admin)])
2208: def arrange_all_ready_to_ship(req: BatchShipmentRequest):
2209: if req.dry_run is False:
2210: if not settings.enable_shipment_writes:
2211: raise HTTPException(403, "Shipment write masih dikunci. Set ENABLE_SHIPMENT_WRITES=true selepas yakin preview betul.")
2212: if not (req.confirmed or req.confirmation == settings.shipment_confirmation_phrase):
2213: raise HTTPException(400, "Sila tekan CONFIRM pada popup sebelum arrange shipment.")
2214: db.clear_cache("ready")
2215: packing_before = db.get_ready_to_ship_packing(source="auto", days=7, limit=req.limit)
2216: candidates = db.list_ready_to_ship_for_arrange(req.limit)
2217: results: list[dict[str, Any]] = []
2218: awb_documents: list[dict[str, Any]] = []
2219: client = ShopeeClient()
2220: try:
2221: for order in candidates:
2222: order_sn = order.get("order_sn")
2223: order_success = True
2224: current_status = db.get_order_status(order_sn)
2225: if current_status != "READY_TO_SHIP":
2226: results.append({
2227: "order_sn": order_sn,
2228: "package_number": None,
2229: "status": "skipped_not_ready",
2230: "current_status": current_status or "MISSING",
2231: "reason": f"Order sudah {current_status.lower() if current_status else 'hilang'} sebelum sempat arrange.",
2232: })
2233: continue
2234: for package_number in (order.get("packages") or [None]):
2235: item: dict[str, Any] = {"order_sn": order_sn, "package_number": package_number, "status": "pending"}
2236: try:
2237: params = client.get_shipping_parameter(order_sn, package_number)
2238: db.save_order_raw(None, current_shop_id(), order_sn, "logistics/get_shipping_parameter/batch_arrange", params)
2239: method, ship_body = _build_auto_ship_body(order_sn, package_number, params, req.method)
2240: item["method"] = method
2241: item["request_preview"] = {k: ("***" if k in {"sender_real_name"} else v) for k, v in ship_body.items()}
2242: if req.dry_run:
2243: item["status"] = "preview_ok"
2244: else:
2245: try:
2246: ship_result = client.ship_order(ship_body)
2247: except Exception as ship_exc:
2248: # Shopee rejects package_number for unsplit orders. Retry once without it.
2249: msg = str(ship_exc).lower()
2250: payload = getattr(ship_exc, "payload", None)
2251: payload_text = str(payload or "").lower()
2252: package_rejected = any(
2253: marker in f"{msg} {payload_text}"
2254: for marker in (
2255: "logistics.package_not_exist",
2256: "package does not exist",
2257: "invalid package number",
2258: "package_number",
2259: "forder_id",
2260: )
2261: )
2262: if package_number and package_rejected:
2263: retry_body = dict(ship_body)
2264: retry_body.pop("package_number", None)
2265: item["retry_without_package_number"] = True
2266: ship_result = client.ship_order(retry_body)
2267: ship_body = retry_body
2268: else:
2269: raise
2270: db.save_order_raw(None, current_shop_id(), order_sn, "logistics/ship_order/batch_arrange", ship_result)
2271: item["status"] = "success"
2272: item["response"] = ship_result.get("response", ship_result)
2273: try:
2274: tracking_pkg = package_number if ship_body.get("package_number") else None
2275: tracking = client.get_tracking_number(order_sn, tracking_pkg)
2276: db.upsert_order_logistics(current_shop_id(), order_sn, tracking_pkg or "", "logistics/get_tracking_number", tracking)
2277: item["tracking"] = tracking.get("response", tracking)
2278: tracking_number = _tracking_number_from_payload(tracking)
2279: except Exception as track_exc:
2280: item["tracking_warning"] = str(track_exc)[:500]
2281: tracking_number = ""
2282: awb_documents.append({
2283: "order_sn": order_sn,
2284: "package_number": str(package_number or ""),
2285: "api_package_number": str(tracking_pkg or ""),
2286: "tracking_number": tracking_number,
2287: })
2288: except Exception as exc:
2289: order_success = False
2290: item["status"] = "failed"
2291: item["error"] = str(exc)[:1000]
2292: payload = getattr(exc, "payload", None)
2293: if payload:
2294: item["payload"] = payload
2295: db.save_order_error(None, current_shop_id(), order_sn, "batch_arrange_shipment", str(exc), getattr(exc, "payload", None))
2296: results.append(item)
2297: if (not req.dry_run) and order_success:
2298: db.mark_order_processed_after_arrange(order_sn, "PROCESSED")
2299: db.log_audit("admin", "batch_arrange_shipment", str(order_sn), {"method": req.method, "packages": order.get("packages")})
2300: finally:
2301: client.close()
2302: success = sum(1 for x in results if x.get("status") == "success")
2303: failed = sum(1 for x in results if x.get("status") == "failed")
2304: preview_ok = sum(1 for x in results if x.get("status") == "preview_ok")
2305: skipped = sum(1 for x in results if str(x.get("status") or "").startswith("skipped"))
2306: awb_job = None if req.dry_run else _start_arranged_awb_prepare(current_shop_id(), awb_documents, "batch_arrange")
2307: return {
2308: "ok": failed == 0,
2309: "dry_run": req.dry_run,
2310: "requested_orders": len(candidates),
2311: "success_count": success,
2312: "preview_ok_count": preview_ok,
2313: "skipped_count": skipped,
2314: "failed_count": failed,
2315: "results": results,
2316: "awb_prepare": awb_job,
2317: "packing": packing_before,
2318: "note": "Packing list ini diambil sebelum status order ditukar kepada PROCESSED, jadi boleh terus print untuk kerja packing.",
2319: }
2320:
2321: @app.post("/api/shipment/arrange", dependencies=[Depends(require_admin)])
2322: def arrange(req:ShipmentRequest):
2323: if not settings.enable_shipment_writes: raise HTTPException(403,"Shipment write masih dikunci")
2324: if not (req.confirmed or req.confirmation == settings.shipment_confirmation_phrase): raise HTTPException(400,"Sila tekan CONFIRM pada popup sebelum arrange shipment")
2325: method=req.method.lower()
2326: if method not in {"pickup","dropoff","non_integrated"}: raise HTTPException(422,"Kaedah shipment tidak sah")
2327: details=getattr(req,method)
2328: if not details: raise HTTPException(422,f"Maklumat {method} diperlukan")
2329: body={"order_sn":req.order_sn,method:details}
2330: if req.package_number: body["package_number"]=req.package_number
2331: client=ShopeeClient()
2332: try:
2333: result=client.ship_order(body); db.log_audit("admin","arrange_shipment",req.order_sn,{"method":method,"package_number":req.package_number})
2334: tracking_number=""
2335: try:
2336: tracking=client.get_tracking_number(req.order_sn,req.package_number or None)
2337: db.upsert_order_logistics(current_shop_id(),req.order_sn,req.package_number or "","logistics/get_tracking_number/arrange",tracking)
2338: tracking_number=_tracking_number_from_payload(tracking)
2339: except Exception as exc:
2340: db.save_order_error(None,current_shop_id(),req.order_sn,"arrange_tracking",str(exc),getattr(exc,"payload",None))
2341: db.mark_order_processed_after_arrange(req.order_sn,"PROCESSED")
2342: awb_job=_start_arranged_awb_prepare(current_shop_id(),[{"order_sn":req.order_sn,"package_number":req.package_number or "","api_package_number":req.package_number or "","tracking_number":tracking_number}],"single_arrange")
2343: return {**result,"awb_prepare":awb_job}
2344: finally: client.close()
2345:
2346:
2347: def _awb_document_key_payload(item: AwbDocumentKey) -> dict[str, str]:
2348: return {
2349: "order_sn": str(item.order_sn or "").strip(),
2350: "package_number": str(item.package_number or "").strip(),
2351: }
2352:
2353:
2354: def _dedupe_awb_document_keys(items: list[AwbDocumentKey]) -> list[dict[str, str]]:
2355: seen: set[tuple[str, str]] = set()
2356: documents: list[dict[str, str]] = []
2357: for item in items:
2358: payload = _awb_document_key_payload(item)
2359: if not payload["order_sn"]:
2360: continue
2361: key = (payload["order_sn"], payload["package_number"])
2362: if key in seen:
2363: continue
2364: seen.add(key)
2365: documents.append(payload)
2366: return documents
2367:
2368:
2369: def _awb_branding_payload(shop_id: int) -> dict[str, Any]:
2370: branding = db.get_awb_shop_branding(shop_id) or {}
2371: logo_path = awb_printing.branding_logo_path(shop_id)
2372: has_logo = bool(branding) and bool(logo_path.exists())
2373: updated_at = branding.get("updated_at") if branding else None
2374: return {
2375: "enabled": bool(branding.get("enabled")) if branding else False,
2376: "has_logo": has_logo,
2377: "logo_updated_at": updated_at,
2378: "logo_sha256": branding.get("logo_sha256") if branding else "",
2379: "original_filename": branding.get("original_filename") if branding else "",
2380: "source_mime_type": branding.get("source_mime_type") if branding else "",
2381: }
2382:
2383:
2384: def _awb_print_insert_payload(shop_id: int) -> dict[str, Any]:
2385: row = db.get_awb_shop_print_insert(shop_id) or {}
2386: image_path = awb_printing.print_insert_path(shop_id)
---HIT 3179 3403 ---
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 [],
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 (
3310: "inventory_products",
3311: "inventory_containers",
3312: "inventory_batches",
3313: "inventory_batch_items",
3314: "label_printer_profiles",
3315: "inventory_publish_policies",
3316: ):
3317: row = conn.execute(
3318: f"SELECT COALESCE(MAX(updated_at),'0'), COUNT(*) FROM {table} WHERE shop_id=?",
3319: (shop_id,),
3320: ).fetchone()
3321: h.update(f"{table}:{row[0]}:{row[1]}".encode())
3322: return h.hexdigest()[:16]
3323:
3324:
3325: def _scanner_forecast_revision(shop_id: int) -> str:
3326: import hashlib as _hl
3327: state = db.get_scanner_forecast_revision(shop_id)
3328: raw = (
3329: f"{int(state.get('forecast_revision') or 0)}:"
3330: f"{state.get('forecast_updated_at') or '0'}:"
3331: f"{state.get('forecast_source') or ''}"
3332: )
3333: return _hl.sha256(raw.encode()).hexdigest()[:16]
3334:
3335:
3336: def _scanner_snapshot_revision(shop_id: int) -> str:
3337: """Legacy combined revision retained for older APK versions."""
3338: import hashlib as _hl
3339: combined = f"{_scanner_catalog_revision(shop_id)}:{_scanner_forecast_revision(shop_id)}"
3340: return _hl.sha256(combined.encode()).hexdigest()[:16]
3341:
3342:
3343: def _scanner_revisions(shop_id: int) -> dict[str, str]:
3344: catalog = _scanner_catalog_revision(shop_id)
3345: forecast = _scanner_forecast_revision(shop_id)
3346: import hashlib as _hl
3347: return {
3348: "catalog_revision": catalog,
3349: "forecast_revision": forecast,
3350: "snapshot_revision": _hl.sha256(f"{catalog}:{forecast}".encode()).hexdigest()[:16],
3351: }
3352:
3353:
3354: @app.get("/api/scanner/bootstrap", dependencies=[Depends(require_scanner)])
3355: def api_scanner_bootstrap():
3356: sid = current_shop_id()
3357: containers = db.list_inventory_containers(sid, status="all", limit=2000)
3358: products = db.list_inventory_products(sid)
3359: catalog = inventory_excel.canonical_catalog(sid)
3360: name_products = db.list_scanner_name_products(sid)
3361: profiles = [item for item in db.list_label_printer_profiles(sid) if str(item.get("calibration_status")) == "active"]
3362: summary = {
3363: "products": len(products),
3364: "active": sum(1 for item in containers if item.get("status") == "active"),
3365: "pending_location": sum(1 for item in containers if item.get("status") == "pending_location"),
3366: "removed": sum(1 for item in containers if item.get("status") == "removed"),
3367: }
3368: batches = db.list_inventory_batches(sid, 50)
3369: batch_items: list[dict[str, Any]] = []
3370: for batch in batches:
3371: batch_items.extend(db.list_inventory_batch_items(sid, str(batch.get("batch_id") or "")))
3372: # Compute revisions only after every lazy catalog read/seed above, otherwise
3373: # this response can immediately look stale and trigger a second bootstrap.
3374: revisions = _scanner_revisions(sid)
3375: return {
3376: "ok": True,
3377: "shop_id": sid,
3378: **revisions,
3379: "summary": summary,
3380: "products": products,
3381: # The scanner keeps this locally so a known container barcode can be
3382: # resolved immediately without a round-trip for every scan.
3383: "containers": containers,
3384: "canonical_catalog": catalog,
3385: "name_products": name_products,
3386: "batches": batches,
3387: "batch_items": batch_items,
3388: "printer_profiles": profiles,
3389: "workbook": inventory_excel.workbook_summary(sid),
3390: "inventory_policy": inventory_publishing.get_policies(sid),
3391: "forecast_input": compact_forecast.build_input(sid),
3392: }
3393:
3394:
3395: @app.get("/api/scanner/instant-delivery-alerts", dependencies=[Depends(require_scanner)])
3396: def api_scanner_instant_delivery_alerts():
3397: """Compact read-only feed used by the Android background monitor."""
3398: overview = db.get_ready_to_ship_overview()
3399: orders = overview.get("instant_delivery_orders") or []
3400: return {
3401: "ok": True,
3402: "server_time": datetime.now(timezone.utc).isoformat(),
3403: "poll_after_seconds": 30,
---HIT 3215 3439 ---
3404: "repeat_after_seconds": 180,
3405: "count": len(orders),
3406: "orders": orders,
3407: }
3408:
3409:
3410: @app.get("/api/scanner/forecast/input", dependencies=[Depends(require_scanner)])
3411: def api_scanner_forecast_input():
3412: return {"ok": True, "forecast_input": compact_forecast.build_input(current_shop_id())}
3413:
3414:
3415: @app.post("/api/scanner/forecast/run", dependencies=[Depends(require_scanner)])
3416: def api_scanner_forecast_run(req: ScannerForecastRequest):
3417: try:
3418: result = compact_forecast.run_for_shop(
3419: current_shop_id(),
3420: start_date=req.start_date.isoformat(),
3421: duration_days=req.duration_days,
3422: service_level=req.service_level,
3423: allocation_percent=req.allocation_percent,
3424: concentrate_percent=req.concentrate_percent,
3425: )
3426: return {"ok": True, "result": result}
3427: except ValueError as exc:
3428: raise HTTPException(422, str(exc)) from exc
3429:
3430:
3431: @app.get("/api/scanner/forecast/leave/preview", dependencies=[Depends(require_scanner)])
3432: def api_scanner_forecast_leave_preview(
3433: start_date: date = Query(...),
3434: duration_days: int = Query(..., ge=1, le=90),
3435: service_level: float = Query(0.99, ge=0.80, le=0.995),
3436: allocation_percent: float | None = Query(default=None, ge=0, le=100),
3437: concentrate_percent: float | None = Query(default=None, ge=0, le=100),
3438: ):
3439: try: