feat(api): add OpenAPI documentation
This commit is contained in:
@@ -64,7 +64,7 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The frontend is available at `http://127.0.0.1:3000`. The backend health endpoint is available at `http://127.0.0.1:8080/health`.
|
The frontend is available at `http://127.0.0.1:3000`. The backend health endpoint is available at `http://127.0.0.1:8080/health`, and the interactive API documentation is available at `http://127.0.0.1:8080/docs`.
|
||||||
|
|
||||||
## Running with Docker Compose
|
## Running with Docker Compose
|
||||||
|
|
||||||
@@ -86,6 +86,8 @@ The database volume is preserved by default. To remove it intentionally, run `do
|
|||||||
|
|
||||||
## API Contract
|
## API Contract
|
||||||
|
|
||||||
|
The OpenAPI 3.0 document is served at `http://127.0.0.1:8080/openapi.json`. Swagger UI is available at `http://127.0.0.1:8080/swagger/`, with `/docs` provided as a shortcut. The OpenAPI document is embedded in the backend binary; only the Swagger UI assets are loaded from a CDN.
|
||||||
|
|
||||||
### Convert a card number to an IBAN
|
### Convert a card number to an IBAN
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -167,5 +169,5 @@ The test suite covers card validation and masking, the conversion service, the Z
|
|||||||
- The frontend normalizes Persian and Arabic digits and removes spaces or hyphens before submission.
|
- The frontend normalizes Persian and Arabic digits and removes spaces or hyphens before submission.
|
||||||
- SQLite is sufficient for this assessment and uses a single writer connection for predictable behavior.
|
- SQLite is sufficient for this assessment and uses a single writer connection for predictable behavior.
|
||||||
- Only the IBAN is exposed to the frontend. Account details, owner information, and raw ZarinHub responses remain internal.
|
- Only the IBAN is exposed to the frontend. Account details, owner information, and raw ZarinHub responses remain internal.
|
||||||
- Retries, Docker, OpenAPI, CI, dark mode, and production infrastructure are intentionally outside the project scope.
|
- Retries, CI, dark mode, and production infrastructure are intentionally outside the project scope.
|
||||||
- The interface uses the existing ZarinPal design system's font and logo assets. The font license is stored in `frontend/public/fonts`.
|
- The interface uses the existing ZarinPal design system's font and logo assets. The font license is stored in `frontend/public/fonts`.
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ func run(logger *slog.Logger) error {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/health", healthHandler)
|
mux.HandleFunc("/health", healthHandler)
|
||||||
mux.HandleFunc("/api/v1/card-to-iban", handler.Convert)
|
mux.HandleFunc("/api/v1/card-to-iban", handler.Convert)
|
||||||
|
httpapi.RegisterDocumentation(mux)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: config.ServerAddr,
|
Addr: config.ServerAddr,
|
||||||
|
|||||||
53
backend/internal/httpapi/docs.go
Normal file
53
backend/internal/httpapi/docs.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed openapi.json
|
||||||
|
var openAPIDocument []byte
|
||||||
|
|
||||||
|
const swaggerUI = `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Card to IBAN API documentation</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => SwaggerUIBundle({
|
||||||
|
url: "/openapi.json",
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [SwaggerUIBundle.presets.apis]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
// RegisterDocumentation registers the OpenAPI document and its Swagger UI.
|
||||||
|
func RegisterDocumentation(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("GET /openapi.json", serveOpenAPI)
|
||||||
|
mux.HandleFunc("GET /swagger/", serveSwaggerUI)
|
||||||
|
mux.HandleFunc("GET /docs", redirectToSwaggerUI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveOpenAPI(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(openAPIDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveSwaggerUI(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(swaggerUI))
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectToSwaggerUI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/swagger/", http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
119
backend/internal/httpapi/docs_test.go
Normal file
119
backend/internal/httpapi/docs_test.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegisterDocumentation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
wantStatus int
|
||||||
|
wantContentType string
|
||||||
|
wantBody string
|
||||||
|
wantLocation string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OpenAPI document",
|
||||||
|
path: "/openapi.json",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantContentType: "application/json",
|
||||||
|
wantBody: `"openapi": "3.0.3"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Swagger UI",
|
||||||
|
path: "/swagger/",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantContentType: "text/html; charset=utf-8",
|
||||||
|
wantBody: `url: "/openapi.json"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "documentation shortcut",
|
||||||
|
path: "/docs",
|
||||||
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
|
wantLocation: "/swagger/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
RegisterDocumentation(mux)
|
||||||
|
request := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mux.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
if response.Code != tt.wantStatus {
|
||||||
|
t.Fatalf("status = %d, want %d", response.Code, tt.wantStatus)
|
||||||
|
}
|
||||||
|
if tt.wantContentType != "" {
|
||||||
|
if got := response.Header().Get("Content-Type"); got != tt.wantContentType {
|
||||||
|
t.Errorf("Content-Type = %q, want %q", got, tt.wantContentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.wantBody != "" && !strings.Contains(response.Body.String(), tt.wantBody) {
|
||||||
|
t.Errorf("body does not contain %q", tt.wantBody)
|
||||||
|
}
|
||||||
|
if got := response.Header().Get("Location"); got != tt.wantLocation {
|
||||||
|
t.Errorf("Location = %q, want %q", got, tt.wantLocation)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAPIDocumentCoversPublicAPI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
RegisterDocumentation(mux)
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/openapi.json", nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
var document struct {
|
||||||
|
OpenAPI string `json:"openapi"`
|
||||||
|
Paths map[string]map[string]struct {
|
||||||
|
Responses map[string]json.RawMessage `json:"responses"`
|
||||||
|
} `json:"paths"`
|
||||||
|
Components struct {
|
||||||
|
Schemas map[string]json.RawMessage `json:"schemas"`
|
||||||
|
} `json:"components"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(response.Body.Bytes(), &document); err != nil {
|
||||||
|
t.Fatalf("decode OpenAPI document: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.OpenAPI != "3.0.3" {
|
||||||
|
t.Errorf("OpenAPI version = %q, want 3.0.3", document.OpenAPI)
|
||||||
|
}
|
||||||
|
if _, ok := document.Paths["/health"]["get"]; !ok {
|
||||||
|
t.Error("OpenAPI document does not define GET /health")
|
||||||
|
}
|
||||||
|
|
||||||
|
conversion, ok := document.Paths["/api/v1/card-to-iban"]["post"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("OpenAPI document does not define POST /api/v1/card-to-iban")
|
||||||
|
}
|
||||||
|
for _, status := range []string{"200", "400", "405", "422", "500", "502", "503", "504"} {
|
||||||
|
if _, ok := conversion.Responses[status]; !ok {
|
||||||
|
t.Errorf("POST /api/v1/card-to-iban does not document response %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, schema := range []string{
|
||||||
|
"ConvertRequest",
|
||||||
|
"ConvertResponse",
|
||||||
|
"ErrorResponse",
|
||||||
|
"HealthResponse",
|
||||||
|
} {
|
||||||
|
if _, ok := document.Components.Schemas[schema]; !ok {
|
||||||
|
t.Errorf("OpenAPI document does not define schema %s", schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
363
backend/internal/httpapi/openapi.json
Normal file
363
backend/internal/httpapi/openapi.json
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Card to IBAN API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Converts an Iranian bank card number to its IBAN."
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "/",
|
||||||
|
"description": "Current server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "System"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Conversion"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["System"],
|
||||||
|
"summary": "Check service health",
|
||||||
|
"operationId": "getHealth",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The service is running.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HealthResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/card-to-iban": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Conversion"],
|
||||||
|
"summary": "Convert a card number to an IBAN",
|
||||||
|
"operationId": "convertCardToIBAN",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConvertRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"card": "6037997512345678"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The card number was converted successfully.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConvertResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"data": {
|
||||||
|
"iban": "IR820540102680020817909002"
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/InvalidCard"
|
||||||
|
},
|
||||||
|
"405": {
|
||||||
|
"$ref": "#/components/responses/MethodNotAllowed"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/components/responses/ConversionRejected"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/InternalError"
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"$ref": "#/components/responses/UpstreamFailure"
|
||||||
|
},
|
||||||
|
"503": {
|
||||||
|
"$ref": "#/components/responses/UpstreamUnavailable"
|
||||||
|
},
|
||||||
|
"504": {
|
||||||
|
"$ref": "#/components/responses/UpstreamTimeout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"headers": {
|
||||||
|
"RequestID": {
|
||||||
|
"description": "Identifier generated for tracing this request.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"HealthResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["status"],
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["ok"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConvertRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["card"],
|
||||||
|
"properties": {
|
||||||
|
"card": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Iranian bank card number containing exactly 16 ASCII digits.",
|
||||||
|
"pattern": "^[0-9]{16}$",
|
||||||
|
"minLength": 16,
|
||||||
|
"maxLength": 16,
|
||||||
|
"example": "6037997512345678"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConvertResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["data", "requestId"],
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/ConvertData"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConvertData": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["iban"],
|
||||||
|
"properties": {
|
||||||
|
"iban": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^IR[0-9]{24}$",
|
||||||
|
"example": "IR820540102680020817909002"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["error", "requestId"],
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"$ref": "#/components/schemas/ErrorDetail"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorDetail": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["code", "message"],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"invalid_card",
|
||||||
|
"method_not_allowed",
|
||||||
|
"conversion_rejected",
|
||||||
|
"internal_error",
|
||||||
|
"upstream_failure",
|
||||||
|
"upstream_unavailable",
|
||||||
|
"upstream_timeout"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-facing error message in Persian."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"InvalidCard": {
|
||||||
|
"description": "The JSON body or card number is invalid.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "invalid_card",
|
||||||
|
"message": "شماره کارت باید شامل ۱۶ رقم باشد."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MethodNotAllowed": {
|
||||||
|
"description": "The request method is not supported for this path.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "method_not_allowed",
|
||||||
|
"message": "این روش برای این مسیر پشتیبانی نمیشود."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversionRejected": {
|
||||||
|
"description": "The upstream service rejected the conversion.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "conversion_rejected",
|
||||||
|
"message": "تبدیل این شماره کارت انجام نشد."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"InternalError": {
|
||||||
|
"description": "An internal operation failed.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "internal_error",
|
||||||
|
"message": "خطایی در پردازش درخواست رخ داد."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamFailure": {
|
||||||
|
"description": "Upstream authentication failed or its response was invalid.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "upstream_failure",
|
||||||
|
"message": "ارتباط با سرویس بانکی با خطا روبهرو شد."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamUnavailable": {
|
||||||
|
"description": "The upstream service is temporarily unavailable.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "upstream_unavailable",
|
||||||
|
"message": "سرویس بانکی موقتاً در دسترس نیست."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamTimeout": {
|
||||||
|
"description": "The upstream request timed out.",
|
||||||
|
"headers": {
|
||||||
|
"X-Request-ID": {
|
||||||
|
"$ref": "#/components/headers/RequestID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"error": {
|
||||||
|
"code": "upstream_timeout",
|
||||||
|
"message": "پاسخ سرویس بانکی بیش از حد طول کشید."
|
||||||
|
},
|
||||||
|
"requestId": "SERVER_GENERATED_REQUEST_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user