{"openapi":"3.1.0","info":{"title":"Shop Dashboard Intake API","version":"2.0.0","description":"Daily-only intake. Only past day records are stored; week, month and MTD are calculated from daily gross sales, refunds, visitors, AOV and product conversion. MTD compares the same date range last month. Existing /api/v1 paths remain; non-day writes now return 422. Internal API without authentication."},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/ingestion":{"post":{"operationId":"ingestMetrics","summary":"Atomically submit partial metrics; retry with the same requestId and body","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Submission"}}}},"responses":{"200":{"description":"Created/updated/stale/unchanged counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeResult"}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/shops":{"get":{"operationId":"listShops","responses":{"200":{"description":"Shop list","content":{"application/json":{"schema":{"type":"object","properties":{"shops":{"type":"array","items":{"$ref":"#/components/schemas/Shop"}}}}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"upsertShop","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopWrite"}}}},"responses":{"200":{"description":"Saved shop","content":{"application/json":{"schema":{"type":"object","properties":{"shop":{"$ref":"#/components/schemas/Shop"}}}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteShop","description":"Deletes the shop and its metrics. Request history is retained. Supply either a single query id or a JSON ids array (1–500); batch deletes are atomic and retryable.","parameters":[{"name":"id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted shop count","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"},"deletedShops":{"type":"integer"}}}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","pattern":"^[A-Za-z0-9_.-]{1,100}$"}}}}}}}}},"/api/v1/shops/import":{"post":{"operationId":"importShops","summary":"Atomically create or update 1–1000 shops by ID","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["shops"],"properties":{"shops":{"type":"array","minItems":1,"maxItems":1000,"items":{"$ref":"#/components/schemas/ShopWrite"}}}}}}},"responses":{"200":{"description":"Import counts","content":{"application/json":{"schema":{"type":"object","required":["total","inserted","updated"],"properties":{"total":{"type":"integer"},"inserted":{"type":"integer"},"updated":{"type":"integer"}}}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/metrics":{"get":{"operationId":"getMetricStatus","parameters":[{"name":"shopId","in":"query","required":true,"schema":{"type":"string"}},{"name":"grain","in":"query","required":true,"schema":{"type":"string","enum":["day","week","month","mtd"]}},{"name":"startDate","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Raw daily metrics or calculated period metrics with coverage and growth","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetricStatus"}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/records":{"get":{"operationId":"listRecords","parameters":[{"name":"shopId","in":"query","required":false,"schema":{"type":"string"}},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["day"]}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100000,"default":1}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":20}},{"name":"startDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["shopId","shopName","grain","startDate","endDate","gmv","refundAmount","aov","visitors","productConversionRate","missingMetrics"],"default":"startDate"}},{"name":"sortOrder","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"Paginated records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPage"}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Only records fully contained within the optional date bounds. Start must not exceed end. Only raw daily records are listed; use /api/v1/metrics for calculated weeks/months/MTD."},"delete":{"operationId":"deleteRecord","parameters":[{"name":"shopId","in":"query","required":false,"schema":{"type":"string"}},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["day"]}},{"name":"startDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","required":false,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Removed metric count","content":{"application/json":{"schema":{"type":"object","properties":{"deletedMetrics":{"type":"integer"}}}}}},"400":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error; no partial writes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Supply either all four query identity fields or JSON records (1–500); validates all records before atomic deletion. History retained. Only day records may be deleted; derived reports recalculate automatically.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"records":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"object","required":["shopId","grain","startDate","endDate"],"properties":{"shopId":{"type":"string","pattern":"^[A-Za-z0-9_.-]{1,100}$"},"grain":{"type":"string","enum":["day"]},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}}}}}}}}}}},"/api/dashboard":{"get":{"operationId":"getDashboard","responses":{"200":{"description":"Database dashboard projection","content":{"application/json":{"schema":{"type":"object","properties":{"snapshot":{"type":"string"},"shops":{"type":"array","items":{"$ref":"#/components/schemas/Shop"}},"periods":{"type":"array","items":{"type":"object"}},"metrics":{"type":"array","items":{"type":"object"}},"daily":{"type":"array","items":{"type":"object"}}}}}}},"503":{"description":"Database unavailable"}}}}},"components":{"schemas":{"Shop":{"type":"object","additionalProperties":false,"required":["account","id","name"],"properties":{"account":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"Shop account; not unique. Null only for legacy shops awaiting completion."},"id":{"type":"string","pattern":"^[A-Za-z0-9_.-]{1,100}$"},"name":{"type":"string","minLength":1,"maxLength":200}}},"Period":{"type":"object","additionalProperties":false,"required":["grain","startDate","endDate"],"properties":{"grain":{"type":"string","enum":["day"]},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}},"description":"Daily input only. startDate must equal endDate and precede the Asia/Shanghai date of collectedAt. week/month/mtd writes are rejected with 422."},"Metrics":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"gmv":{"description":"Gross transaction amount (positive), BEFORE refunds. Nonnegative CNY, maximum 2 decimal places. Dashboard GMV = this gmv field minus refundAmount. This intake field retains its existing name.","oneOf":[{"type":"number"},{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}]},"refundAmount":{"description":"Nonnegative refund amount in CNY; maximum 2 decimal places.","oneOf":[{"type":"number"},{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}]},"aov":{"description":"Source AOV in CNY; nonnegative; maximum 6 decimal places.","oneOf":[{"type":"number"},{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}]},"visitors":{"oneOf":[{"type":"integer","minimum":0,"maximum":999999999999999},{"type":"string","pattern":"^\\d{1,15}$"}]},"productConversionRate":{"description":"Product conversion rate 0..1, up to 8 decimal places. 2.5% = 0.025.","oneOf":[{"type":"number"},{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$"}]},"parentOrders":{"oneOf":[{"type":"integer","minimum":0,"maximum":999999999999999},{"type":"string","pattern":"^\\d{1,15}$"}]}},"description":"Omitted fields are unchanged; null is invalid. Decimal strings recommended. Visitors and parentOrders also accept integer decimal strings."},"Record":{"type":"object","additionalProperties":false,"required":["shopId","period","collectedAt","metrics"],"properties":{"shopId":{"type":"string","pattern":"^[A-Za-z0-9_.-]{1,100}$"},"shopAccount":{"type":"string","minLength":1,"maxLength":200,"description":"Required for a new shop. Omitted on existing shops preserves the account."},"shopName":{"type":"string","minLength":1,"maxLength":200,"description":"Required together with shopAccount for a new shop; when supplied updates the name."},"period":{"$ref":"#/components/schemas/Period"},"collectedAt":{"type":"string","format":"date-time","description":"Actual collection timestamp with timezone; at most milliseconds. Cannot be >5 minutes in future."},"metrics":{"$ref":"#/components/schemas/Metrics"}}},"Submission":{"type":"object","additionalProperties":false,"required":["requestId","source","records"],"properties":{"requestId":{"type":"string","format":"uuid"},"source":{"type":"string","minLength":1,"maxLength":100},"records":{"type":"array","minItems":1,"maxItems":500,"items":{"$ref":"#/components/schemas/Record"}}}},"IntakeResult":{"type":"object","required":["requestId","records","metrics","inserted","updated","stale","unchanged","replayed"],"properties":{"requestId":{"type":"string","format":"uuid"},"records":{"type":"integer","minimum":0},"metrics":{"type":"integer","minimum":0},"inserted":{"type":"integer","minimum":0},"updated":{"type":"integer","minimum":0},"stale":{"type":"integer","minimum":0},"unchanged":{"type":"integer","minimum":0},"replayed":{"type":"boolean"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"array","items":{}}}}}},"MetricValue":{"type":"object","properties":{"value":{"type":"string"},"collectedAt":{"type":"string","format":"date-time"},"source":{"type":"string"},"requestId":{"type":"string","format":"uuid"}}},"MetricStatus":{"type":"object","properties":{"shopId":{"type":"string"},"period":{"allOf":[{"$ref":"#/components/schemas/ReadPeriod"}],"description":"Also includes canonical id"},"metrics":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MetricValue"}},"complete":{"type":"boolean"},"missingMetrics":{"type":"array","items":{"type":"string"}},"calculated":{"type":"boolean"},"netGmv":{"type":["number","null"],"description":"Net GMV after refunds; metrics.gmv remains gross sales."},"coverage":{"type":"object","properties":{"recordedDays":{"type":"integer"},"expectedDays":{"type":"integer"},"completeDays":{"type":"integer"},"metricDays":{"type":"object","additionalProperties":{"type":"integer"}}}},"growth":{"type":"object","additionalProperties":{"type":["number","null"]}}}},"RecordPage":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"records":{"type":"array","items":{"type":"object","properties":{"shopId":{"type":"string"},"shopName":{"type":"string"},"periodId":{"type":"string"},"grain":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"},"metrics":{"type":"object","additionalProperties":{"type":"string"}},"collectedAt":{"type":"string","format":"date-time"},"missingMetrics":{"type":"array","items":{"type":"string"}}}}}}},"ShopWrite":{"type":"object","additionalProperties":false,"required":["account","id","name"],"properties":{"account":{"type":"string","minLength":1,"maxLength":200,"description":"Required account. Multiple shops may share an account; id is the unique key."},"id":{"type":"string","pattern":"^[A-Za-z0-9_.-]{1,100}$"},"name":{"type":"string","minLength":1,"maxLength":200}}},"ReadPeriod":{"type":"object","additionalProperties":false,"required":["grain","startDate","endDate"],"properties":{"grain":{"type":"string","enum":["day","week","month","mtd"]},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}},"description":"Read range: day is one date; week is Monday through Sunday; month is the full calendar month; mtd is month start through an explicit cutoff strictly before today in Asia/Shanghai. All derived ranges calculate from daily inputs and exclude today/future dates."}}}}