Files
doormile_backend/utils/response_cx.go
Suriyakumarvijayanayagam f1dbf7edc9 feat: interim customer PIN auth (login/set-pin/verify-pin), mirrors miler flow
No SMS/OTP gateway is live yet, so customers sign in with a self-set PIN like
milers do. The OTP endpoints stay in place — the app switches back once a
gateway is plugged in.

- POST /customer/auth/login  {phone} -> {registered, pin_set, name}: routes the
  app to register / set-PIN / enter-PIN.
- POST /customer/auth/set-pin {phone, new_pin, name?}: first-time PIN. Creates
  the account (name required) or sets the first PIN on an account with none;
  refuses to overwrite an existing PIN (409); logs in on success.
- POST /customer/auth/verify-pin {phone, pin}: returning login; same generic
  message for unknown phone and wrong PIN so it can't enumerate accounts.

All three reuse issueCxSession (access + refresh + customer) and the /customer
Cx* response envelope. Build + vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRaFH5hMRqmUQvVPQsyjZD
2026-09-21 16:25:03 +05:30

101 lines
3.5 KiB
Go

package utils
import "github.com/gofiber/fiber/v2"
// Customer-app (doormile_cx) response envelope.
//
// Deliberately a separate helper set from OK/List/Fail rather than a reuse of
// them. The customer contract fixes three things the miler/console contract
// does not: `message` is always present on success (empty string, never
// omitted), the machine-readable code is nested under `error.code` rather than
// sitting at the top level, and `total`/`nextCursor` ride the list envelope.
// Serving one endpoint in one shape and its neighbour in another is exactly
// what cost the miler client a release on /miler/verify-pin — so the customer
// surface gets its own helpers and every /customer/* response goes through
// them, auth included.
// CxErr* are the error codes in the customer contract. The client branches on
// these; `message` is customer-safe English shown verbatim in the UI.
const (
CxErrInvalid = "invalid"
CxErrInvalidName = "invalid_name"
CxErrInvalidOtp = "invalid_otp"
CxErrInvalidPin = "invalid_pin"
CxErrPinNotSet = "pin_not_set"
CxErrPinAlreadySet = "pin_already_set"
CxErrUnauthorized = "unauthorized"
CxErrForbidden = "forbidden"
CxErrNotFound = "not_found"
CxErrConflict = "conflict"
CxErrUnserviceable = "unserviceable"
CxErrRateLimited = "rate_limited"
CxErrServer = "server_error"
)
// CxOK is the success envelope: {success, data, message}.
func CxOK(c *fiber.Ctx, data interface{}) error {
return c.JSON(fiber.Map{"success": true, "data": data, "message": ""})
}
// CxCreated is CxOK with a 201.
func CxCreated(c *fiber.Ctx, data interface{}) error {
return c.Status(fiber.StatusCreated).
JSON(fiber.Map{"success": true, "data": data, "message": ""})
}
// CxList is the list envelope. data is always an array — never null, because
// the client types it as a list and a null throws in the parser. nextCursor is
// null when the page is the last one.
func CxList(c *fiber.Ctx, data interface{}, total int, nextCursor *string) error {
body := fiber.Map{
"success": true,
"data": data,
"total": total,
"nextCursor": nil,
"message": "",
}
if nextCursor != nil && *nextCursor != "" {
body["nextCursor"] = *nextCursor
}
return c.JSON(body)
}
// CxFail is the error envelope: {success:false, message, error:{code}}. The
// app funnels every failure into one retryable error state and renders
// `message` verbatim, so callers must pass customer-safe English here — never
// an enum key, a driver error or a wrapped stack.
func CxFail(c *fiber.Ctx, status int, code, msg string) error {
return c.Status(status).JSON(fiber.Map{
"success": false,
"message": msg,
"error": fiber.Map{"code": code},
})
}
// Shorthands for the statuses the contract pins to a specific code.
func CxBadRequest(c *fiber.Ctx, msg string) error {
return CxFail(c, fiber.StatusBadRequest, CxErrInvalid, msg)
}
func CxUnauthorized(c *fiber.Ctx, msg string) error {
return CxFail(c, fiber.StatusUnauthorized, CxErrUnauthorized, msg)
}
func CxForbidden(c *fiber.Ctx, msg string) error {
return CxFail(c, fiber.StatusForbidden, CxErrForbidden, msg)
}
func CxNotFound(c *fiber.Ctx, msg string) error {
return CxFail(c, fiber.StatusNotFound, CxErrNotFound, msg)
}
func CxConflict(c *fiber.Ctx, msg string) error {
return CxFail(c, fiber.StatusConflict, CxErrConflict, msg)
}
// CxInternal never leaks the underlying error to the customer. Log the real
// one; the app shows this.
func CxInternal(c *fiber.Ctx) error {
return CxFail(c, fiber.StatusInternalServerError, CxErrServer, "Something went wrong")
}