feat(orders): implement QA audit fixes — return flow, refund webhook, TS cleanup

Convex backend (AUDIT-5–10):
- schema: add returnLabelUrl, returnTrackingNumber, returnCarrier fields +
  by_return_tracking_number_and_carrier and by_stripe_payment_intent_id indexes
- orders: markReturnReceived now sets status="completed"; add getOrderByPaymentIntent
  and applyReturnAccepted internal helpers
- returnActions: add acceptReturn action — creates Shippo return label (is_return:true),
  persists label data, sends return label email to customer
- stripeActions: handle refund.updated webhook to auto-mark orders refunded via Stripe Dashboard
- shippoWebhook: add getOrderByReturnTracking; applyTrackingUpdate extended with
  isReturnTracking flag (return events use return_tracking_update type, skip delivered transition)
- emails: add sendReturnLabelEmail; fulfillmentActions: createShippingLabel action

Admin UI (AUDIT-1–6):
- OrderActionsBar: full rewrite per authoritative action matrix; remove UpdateStatusDialog;
  add AcceptReturnButton for delivered+returnRequested state
- AcceptReturnButton: new action component matching CreateLabelButton pattern
- FulfilmentCard: add returnLabelUrl prop; show "Return label" row; rename outbound
  label to "Outbound label" when both are present
- statusConfig: add return_accepted to OrderEventType and EVENT_TYPE_LABELS
- orders detail page and all supporting cards/components

Storefront & shared (TS fixes):
- checkout/success, CheckoutErrorState, OrderReviewStep, PaymentStep: replace
  Button as/color/isLoading/variant="flat" with HeroUI v3-compatible props
- ReviewList: isLoading → isPending for HeroUI v3 Button
- packages/utils: add return and completed entries to ORDER_STATUS_LABELS/COLORS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 17:59:29 +03:00
parent 8e4309892c
commit 3d50cb895c
32 changed files with 3046 additions and 45 deletions

View File

@@ -167,6 +167,8 @@ export default defineSchema({
v.literal("delivered"),
v.literal("cancelled"),
v.literal("refunded"),
v.literal("return"),
v.literal("completed"),
),
paymentStatus: v.union(
v.literal("pending"),
@@ -209,6 +211,8 @@ export default defineSchema({
carrier: v.string(),
trackingNumber: v.optional(v.string()),
trackingUrl: v.optional(v.string()),
labelUrl: v.optional(v.string()),
trackingStatus: v.optional(v.string()),
estimatedDelivery: v.optional(v.number()),
actualDelivery: v.optional(v.number()),
notes: v.optional(v.string()),
@@ -216,6 +220,11 @@ export default defineSchema({
updatedAt: v.number(),
paidAt: v.optional(v.number()),
shippedAt: v.optional(v.number()),
returnRequestedAt: v.optional(v.number()),
returnReceivedAt: v.optional(v.number()),
returnLabelUrl: v.optional(v.string()),
returnTrackingNumber: v.optional(v.string()),
returnCarrier: v.optional(v.string()),
})
.index("by_user", ["userId"])
.index("by_status", ["status"])
@@ -223,7 +232,10 @@ export default defineSchema({
.index("by_order_number", ["orderNumber"])
.index("by_email", ["email"])
.index("by_created_at", ["createdAt"])
.index("by_stripe_checkout_session_id", ["stripeCheckoutSessionId"]),
.index("by_stripe_checkout_session_id", ["stripeCheckoutSessionId"])
.index("by_tracking_number_and_carrier", ["trackingNumber", "carrier"])
.index("by_return_tracking_number_and_carrier", ["returnTrackingNumber", "returnCarrier"])
.index("by_stripe_payment_intent_id", ["stripePaymentIntentId"]),
orderItems: defineTable({
orderId: v.id("orders"),
@@ -237,6 +249,19 @@ export default defineSchema({
imageUrl: v.optional(v.string()),
}).index("by_order", ["orderId"]),
orderTimelineEvents: defineTable({
orderId: v.id("orders"),
eventType: v.string(), // "status_change" | "customer_cancel" | "return_requested" | "return_received" | "refund" | "tracking_update" | "label_created"
source: v.string(), // "stripe_webhook" | "fulfillment" | "admin" | "shippo_webhook" | "customer_cancel" | "customer_return"
fromStatus: v.optional(v.string()),
toStatus: v.optional(v.string()),
payload: v.optional(v.string()), // JSON string for Shippo/Stripe payloads
createdAt: v.number(),
userId: v.optional(v.id("users")),
})
.index("by_order", ["orderId"])
.index("by_order_and_created_at", ["orderId", "createdAt"]),
// ─── Reviews ───────────────────────────────────────────────────────────
reviews: defineTable({
productId: v.id("products"),