diff --git a/docs/website-api/AI_PROMPT.md b/docs/website-api/AI_PROMPT.md index 2205368..280dafc 100644 --- a/docs/website-api/AI_PROMPT.md +++ b/docs/website-api/AI_PROMPT.md @@ -36,7 +36,8 @@ You are building a **Meshkee business website (storefront)**. You must use the M 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl` - Response includes `payment.redirect` `{ method, url, fields }` — POST/redirect shopper to the bank - - Bank callback hits API then redirects to `returnUrl?status=success|failed&orderId=…` + - Meshkee registers the bank `callback_url` on the **store apex** (`https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback`), which nginx proxies to the API. ZarinPal/Mellat domain checks must match the store domain, not `api.meshkee.com`. + - After verify, API redirects the browser to `returnUrl?status=success|failed&orderId=…` - Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods` ### User products (customer listings) diff --git a/docs/website-api/Meshkee-Website-API.postman_collection.json b/docs/website-api/Meshkee-Website-API.postman_collection.json index 2121d56..be3e6e5 100644 --- a/docs/website-api/Meshkee-Website-API.postman_collection.json +++ b/docs/website-api/Meshkee-Website-API.postman_collection.json @@ -1705,7 +1705,7 @@ "raw": "{\n \"addressId\": \"{{addressId}}\",\n \"customerNotes\": \"Deliver after 5pm\",\n \"returnUrl\": \"https://YOUR_WEBSITE_DOMAIN/checkout/result\",\n \"payment\": {\n \"type\": \"e_payment_gate\",\n \"gatewayType\": \"mellat\"\n }\n}" }, "url": "{{baseUrl}}/businesses/{{businessId}}/cart/checkout", - "description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, bank hits /payments/{gateway}/callback which redirects to returnUrl?status=success|failed." + "description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, the bank hits https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback (nginx → API), which redirects to returnUrl?status=success|failed." } } ] @@ -1723,7 +1723,64 @@ } }, { - "name": "Mellat callback (bank → API)", + "name": "Mellat callback via store domain (gateway → nginx → API)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ResCode", + "value": "0" + }, + { + "key": "SaleOrderId", + "value": "{{transactionId}}" + }, + { + "key": "SaleReferenceId", + "value": "123456789" + }, + { + "key": "RefId", + "value": "{{refId}}" + } + ] + }, + "url": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/mellat/callback", + "description": "Preferred path: bank posts to the store apex; nginx proxies to /tenants/{host}/payments/mellat/callback. Verifies+settles, then 303 redirect to returnUrl." + } + }, + { + "name": "ZarinPal callback via store domain (gateway → nginx → API)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/zarinpal/callback?Authority={{authority}}&Status=OK", + "host": ["YOUR_WEBSITE_DOMAIN"], + "path": [ + "meshkee", + "payments", + "zarinpal", + "callback" + ], + "query": [ + { "key": "Authority", "value": "{{authority}}" }, + { "key": "Status", "value": "OK" } + ] + }, + "description": "Preferred path: ZarinPal redirects to the store apex; nginx proxies to /tenants/{host}/payments/zarinpal/callback. Verifies payment, then 303 redirect to returnUrl." + } + }, + { + "name": "Mellat callback (legacy API URL)", "request": { "method": "POST", "header": [ @@ -1754,11 +1811,11 @@ ] }, "url": "{{baseUrl}}/businesses/{{businessId}}/payments/mellat/callback", - "description": "Called by Mellat (not by the website). Verifies+settles, then 303 redirect to returnUrl." + "description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so gateway domain checks match the merchant terminal." } }, { - "name": "ZarinPal callback (gateway → API)", + "name": "ZarinPal callback (legacy API URL)", "request": { "method": "GET", "header": [], @@ -1777,7 +1834,7 @@ { "key": "Status", "value": "OK" } ] }, - "description": "Called by ZarinPal (not by the website). Verifies payment, then 303 redirect to returnUrl." + "description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so ZarinPal domain checks match the merchant terminal." } } ] diff --git a/docs/website-api/openapi.json b/docs/website-api/openapi.json index a7a8180..828bf16 100644 --- a/docs/website-api/openapi.json +++ b/docs/website-api/openapi.json @@ -3210,7 +3210,8 @@ "tags": [ "Payments" ], - "summary": "Bank payment callback (public; redirects browser to returnUrl)", + "summary": "Bank payment callback — legacy direct API URL (prefer store-domain path)", + "description": "Still supported. Prefer https://{store}/meshkee/payments/{gateway}/callback so gateway domain checks match the merchant terminal.", "parameters": [ { "$ref": "#/components/parameters/businessId" @@ -3255,7 +3256,7 @@ "tags": [ "Payments" ], - "summary": "Bank payment callback (GET variant)", + "summary": "Bank payment callback GET — legacy direct API URL", "parameters": [ { "$ref": "#/components/parameters/businessId" @@ -3282,6 +3283,96 @@ } } } + }, + "/tenants/{host}/payments/{gateway}/callback": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback via store host (nginx proxies /meshkee/payments/... here)", + "description": "Public. Resolves business from host. Used after storefront nginx proxies https://{store}/meshkee/payments/{gateway}/callback.", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + }, + "get": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback GET via store host (ZarinPal)", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + } } } } diff --git a/scripts/websites-agent/README.md b/scripts/websites-agent/README.md index 8fe8800..5c28790 100644 --- a/scripts/websites-agent/README.md +++ b/scripts/websites-agent/README.md @@ -11,6 +11,7 @@ # # Scripts on the VM (same folder): # patch-nginx-seo.sh — add /sitemap.xml + /robots.txt API proxies to an existing nginx site +# patch-nginx-pay.sh — add /meshkee/payments/{zarinpal|mellat}/callback API proxies # # Env (.env): PORT, DEPLOY_TOKEN, ALLOWED_SLUGS # diff --git a/scripts/websites-agent/patch-nginx-pay.sh b/scripts/websites-agent/patch-nginx-pay.sh new file mode 100755 index 0000000..30fe023 --- /dev/null +++ b/scripts/websites-agent/patch-nginx-pay.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Insert / update Meshkee payment-callback proxy locations on a storefront nginx site. +# Bank gateways (ZarinPal/Mellat) hit https://{host}/meshkee/payments/{gateway}/callback +# which proxies to the API tenant callback route. +set -euo pipefail + +HOST="${1:-}" +API_HOST="${MESHKEE_API_HOST:-api.meshkee.com}" + +if [[ -z "$HOST" ]]; then + echo "usage: patch-nginx-pay.sh " >&2 + exit 1 +fi + +NGINX_AVAILABLE="/etc/nginx/sites-available/$HOST" + +if [[ ! -f "$NGINX_AVAILABLE" ]]; then + echo "missing nginx site for $HOST" >&2 + exit 1 +fi + +proxy_location() { + local gateway="$1" + local path="/meshkee/payments/${gateway}/callback" + cat <"$tmp" + mv "$tmp" "$NGINX_AVAILABLE" + echo "added: ${path}" +} + +ensure_location zarinpal +ensure_location mellat + +nginx -t +systemctl reload nginx +echo "patched nginx payment callback locations for $HOST" diff --git a/scripts/websites-agent/provision.sh b/scripts/websites-agent/provision.sh index e73b4f3..cd6b6ff 100755 --- a/scripts/websites-agent/provision.sh +++ b/scripts/websites-agent/provision.sh @@ -162,6 +162,28 @@ server { add_header Cache-Control "public, max-age=900"; } + location = /meshkee/payments/zarinpal/callback { + proxy_pass https://api.meshkee.com/api/v1/tenants/${HOST}/payments/zarinpal/callback; + proxy_set_header Host api.meshkee.com; + proxy_ssl_server_name on; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_pass_request_body on; + proxy_pass_request_headers on; + } + + location = /meshkee/payments/mellat/callback { + proxy_pass https://api.meshkee.com/api/v1/tenants/${HOST}/payments/mellat/callback; + proxy_set_header Host api.meshkee.com; + proxy_ssl_server_name on; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_pass_request_body on; + proxy_pass_request_headers on; + } + location / { proxy_pass http://127.0.0.1:${PORT}; proxy_http_version 1.1; @@ -185,6 +207,9 @@ else if [[ -x "$SCRIPT_DIR/patch-nginx-seo.sh" ]]; then "$SCRIPT_DIR/patch-nginx-seo.sh" "$HOST" || true fi + if [[ -x "$SCRIPT_DIR/patch-nginx-pay.sh" ]]; then + "$SCRIPT_DIR/patch-nginx-pay.sh" "$HOST" || true + fi fi # SSL is issued separately via ssl.sh / Super Admin "Issue SSL" — do not run diff --git a/src/payments/gateways/zarinpal.gateway.ts b/src/payments/gateways/zarinpal.gateway.ts index fa7e015..96d4d77 100644 --- a/src/payments/gateways/zarinpal.gateway.ts +++ b/src/payments/gateways/zarinpal.gateway.ts @@ -86,11 +86,21 @@ export class ZarinpalGateway implements PaymentGatewayAdapter { const code = body.data?.code; const authority = body.data?.authority?.trim() ?? ''; if (code !== ZARINPAL_OK || !authority) { + const errorMsg = + Array.isArray(body.errors) && body.errors[0] && typeof body.errors[0] === 'object' + ? String( + (body.errors[0] as { message?: string }).message ?? + body.data?.message ?? + '', + ).trim() + : (body.data?.message ?? '').trim(); this.logger.warn( - `ZarinPal request failed business=${input.businessId} order=${input.orderId} code=${code ?? 'unknown'}`, + `ZarinPal request failed business=${input.businessId} order=${input.orderId} code=${code ?? 'unknown'} msg=${errorMsg || 'n/a'}`, ); throw new BadGatewayException( - `ZarinPal payment init failed (code ${code ?? 'unknown'})`, + errorMsg + ? `ZarinPal: ${errorMsg} (code ${code ?? 'unknown'})` + : `ZarinPal payment init failed (code ${code ?? 'unknown'})`, ); } diff --git a/src/payments/payments.module.ts b/src/payments/payments.module.ts index 046fbae..31710a6 100644 --- a/src/payments/payments.module.ts +++ b/src/payments/payments.module.ts @@ -1,14 +1,16 @@ import { Module } from '@nestjs/common'; import { BusinessSettingsModule } from '../business-settings/business-settings.module'; +import { TenantModule } from '../tenant/tenant.module'; import { MellatGateway } from './gateways/mellat.gateway'; import { ZarinpalGateway } from './gateways/zarinpal.gateway'; import { PaymentGatewayRegistry } from './payment-gateway.registry'; import { PaymentsController } from './payments.controller'; import { PaymentsService } from './payments.service'; +import { TenantPaymentsController } from './tenant-payments.controller'; @Module({ - imports: [BusinessSettingsModule], - controllers: [PaymentsController], + imports: [BusinessSettingsModule, TenantModule], + controllers: [PaymentsController, TenantPaymentsController], providers: [ MellatGateway, ZarinpalGateway, diff --git a/src/payments/payments.service.ts b/src/payments/payments.service.ts index 0a1f323..73beaa5 100644 --- a/src/payments/payments.service.ts +++ b/src/payments/payments.service.ts @@ -143,7 +143,7 @@ export class PaymentsService { } const adapter = this.gateways.get(input.gatewayType); - const callbackUrl = this.buildCallbackUrl( + const callbackUrl = await this.buildCallbackUrl( input.businessId, input.gatewayType, ); @@ -320,7 +320,16 @@ export class PaymentsService { }; } - private buildCallbackUrl(businessId: bigint, gatewayType: string) { + /** + * Prefer storefront apex so ZarinPal/Mellat domain checks match the merchant + * terminal. Nginx on the store proxies /meshkee/payments/... to the API. + */ + private async buildCallbackUrl(businessId: bigint, gatewayType: string) { + const storeHost = await this.resolveStorefrontCallbackHost(businessId); + if (storeHost) { + return `https://${storeHost}/meshkee/payments/${gatewayType}/callback`; + } + const base = this.config.get('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') || `http://localhost:${this.config.get('PORT') || '3000'}`; @@ -328,6 +337,28 @@ export class PaymentsService { return `${base}/api/v1/businesses/${businessId.toString()}/payments/${gatewayType}/callback`; } + /** Active primary domain, else first active domain; strip www. for gateway match. */ + private async resolveStorefrontCallbackHost( + businessId: bigint, + ): Promise { + const primary = await this.prisma.domain.findFirst({ + where: { businessId, isActive: true, isPrimary: true }, + select: { host: true }, + orderBy: { id: 'asc' }, + }); + const fallback = primary + ? null + : await this.prisma.domain.findFirst({ + where: { businessId, isActive: true }, + select: { host: true }, + orderBy: { id: 'asc' }, + }); + + const host = (primary?.host || fallback?.host || '').trim().toLowerCase(); + if (!host) return null; + return host.startsWith('www.') ? host.slice(4) : host; + } + private async findCallbackTransaction( businessId: bigint, gatewayType: string, diff --git a/src/payments/tenant-payments.controller.ts b/src/payments/tenant-payments.controller.ts new file mode 100644 index 0000000..b7d6f9d --- /dev/null +++ b/src/payments/tenant-payments.controller.ts @@ -0,0 +1,57 @@ +import { + Body, + Controller, + Get, + Param, + Post, + Query, + Res, +} from '@nestjs/common'; +import type { Response } from 'express'; +import { TenantService } from '../tenant/tenant.service'; +import { PaymentsService } from './payments.service'; + +/** + * Storefront-domain bank callbacks (proxied by nginx from + * https://{store}/meshkee/payments/{gateway}/callback). + * Keeps ZarinPal/Mellat domain checks aligned with the merchant terminal. + */ +@Controller('tenants/:host/payments') +export class TenantPaymentsController { + constructor( + private readonly tenant: TenantService, + private readonly payments: PaymentsService, + ) {} + + @Post(':gateway/callback') + async callbackPost( + @Param('host') host: string, + @Param('gateway') gateway: string, + @Body() body: Record, + @Res() res: Response, + ) { + const business = await this.tenant.resolveBusinessByDomain(host); + const result = await this.payments.handleGatewayCallback( + business.id.toString(), + gateway, + body ?? {}, + ); + return res.redirect(303, result.redirectUrl); + } + + @Get(':gateway/callback') + async callbackGet( + @Param('host') host: string, + @Param('gateway') gateway: string, + @Query() query: Record, + @Res() res: Response, + ) { + const business = await this.tenant.resolveBusinessByDomain(host); + const result = await this.payments.handleGatewayCallback( + business.id.toString(), + gateway, + query ?? {}, + ); + return res.redirect(303, result.redirectUrl); + } +} diff --git a/src/website-docs/static/AI_PROMPT.md b/src/website-docs/static/AI_PROMPT.md index 2205368..280dafc 100644 --- a/src/website-docs/static/AI_PROMPT.md +++ b/src/website-docs/static/AI_PROMPT.md @@ -36,7 +36,8 @@ You are building a **Meshkee business website (storefront)**. You must use the M 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl` - Response includes `payment.redirect` `{ method, url, fields }` — POST/redirect shopper to the bank - - Bank callback hits API then redirects to `returnUrl?status=success|failed&orderId=…` + - Meshkee registers the bank `callback_url` on the **store apex** (`https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback`), which nginx proxies to the API. ZarinPal/Mellat domain checks must match the store domain, not `api.meshkee.com`. + - After verify, API redirects the browser to `returnUrl?status=success|failed&orderId=…` - Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods` ### User products (customer listings) diff --git a/src/website-docs/static/Meshkee-Website-API.postman_collection.json b/src/website-docs/static/Meshkee-Website-API.postman_collection.json index 2121d56..be3e6e5 100644 --- a/src/website-docs/static/Meshkee-Website-API.postman_collection.json +++ b/src/website-docs/static/Meshkee-Website-API.postman_collection.json @@ -1705,7 +1705,7 @@ "raw": "{\n \"addressId\": \"{{addressId}}\",\n \"customerNotes\": \"Deliver after 5pm\",\n \"returnUrl\": \"https://YOUR_WEBSITE_DOMAIN/checkout/result\",\n \"payment\": {\n \"type\": \"e_payment_gate\",\n \"gatewayType\": \"mellat\"\n }\n}" }, "url": "{{baseUrl}}/businesses/{{businessId}}/cart/checkout", - "description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, bank hits /payments/{gateway}/callback which redirects to returnUrl?status=success|failed." + "description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, the bank hits https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback (nginx → API), which redirects to returnUrl?status=success|failed." } } ] @@ -1723,7 +1723,64 @@ } }, { - "name": "Mellat callback (bank → API)", + "name": "Mellat callback via store domain (gateway → nginx → API)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ResCode", + "value": "0" + }, + { + "key": "SaleOrderId", + "value": "{{transactionId}}" + }, + { + "key": "SaleReferenceId", + "value": "123456789" + }, + { + "key": "RefId", + "value": "{{refId}}" + } + ] + }, + "url": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/mellat/callback", + "description": "Preferred path: bank posts to the store apex; nginx proxies to /tenants/{host}/payments/mellat/callback. Verifies+settles, then 303 redirect to returnUrl." + } + }, + { + "name": "ZarinPal callback via store domain (gateway → nginx → API)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/zarinpal/callback?Authority={{authority}}&Status=OK", + "host": ["YOUR_WEBSITE_DOMAIN"], + "path": [ + "meshkee", + "payments", + "zarinpal", + "callback" + ], + "query": [ + { "key": "Authority", "value": "{{authority}}" }, + { "key": "Status", "value": "OK" } + ] + }, + "description": "Preferred path: ZarinPal redirects to the store apex; nginx proxies to /tenants/{host}/payments/zarinpal/callback. Verifies payment, then 303 redirect to returnUrl." + } + }, + { + "name": "Mellat callback (legacy API URL)", "request": { "method": "POST", "header": [ @@ -1754,11 +1811,11 @@ ] }, "url": "{{baseUrl}}/businesses/{{businessId}}/payments/mellat/callback", - "description": "Called by Mellat (not by the website). Verifies+settles, then 303 redirect to returnUrl." + "description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so gateway domain checks match the merchant terminal." } }, { - "name": "ZarinPal callback (gateway → API)", + "name": "ZarinPal callback (legacy API URL)", "request": { "method": "GET", "header": [], @@ -1777,7 +1834,7 @@ { "key": "Status", "value": "OK" } ] }, - "description": "Called by ZarinPal (not by the website). Verifies payment, then 303 redirect to returnUrl." + "description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so ZarinPal domain checks match the merchant terminal." } } ] diff --git a/src/website-docs/static/openapi.json b/src/website-docs/static/openapi.json index a7a8180..828bf16 100644 --- a/src/website-docs/static/openapi.json +++ b/src/website-docs/static/openapi.json @@ -3210,7 +3210,8 @@ "tags": [ "Payments" ], - "summary": "Bank payment callback (public; redirects browser to returnUrl)", + "summary": "Bank payment callback — legacy direct API URL (prefer store-domain path)", + "description": "Still supported. Prefer https://{store}/meshkee/payments/{gateway}/callback so gateway domain checks match the merchant terminal.", "parameters": [ { "$ref": "#/components/parameters/businessId" @@ -3255,7 +3256,7 @@ "tags": [ "Payments" ], - "summary": "Bank payment callback (GET variant)", + "summary": "Bank payment callback GET — legacy direct API URL", "parameters": [ { "$ref": "#/components/parameters/businessId" @@ -3282,6 +3283,96 @@ } } } + }, + "/tenants/{host}/payments/{gateway}/callback": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback via store host (nginx proxies /meshkee/payments/... here)", + "description": "Public. Resolves business from host. Used after storefront nginx proxies https://{store}/meshkee/payments/{gateway}/callback.", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + }, + "get": { + "tags": [ + "Payments" + ], + "summary": "Bank payment callback GET via store host (ZarinPal)", + "parameters": [ + { + "name": "host", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "YOUR_WEBSITE_DOMAIN" + } + }, + { + "name": "gateway", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "mellat", + "sep", + "snappay", + "digipay", + "zarinpal" + ] + } + } + ], + "responses": { + "303": { + "description": "Redirect to checkout returnUrl with status=success|failed" + } + } + } } } }