2 Commits
Author SHA1 Message Date
Alireza HassaniandCursor e942348b1d Route payment bank callbacks through storefront domains.
ZarinPal rejects api.meshkee.com when the terminal is registered to the store apex; nginx now proxies /meshkee/payments/{gateway}/callback to a host-scoped API route.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 21:34:56 +03:30
Alireza HassaniandCursor 40284df880 Surface the real build error when website deploy.sh fails.
Super Admin was only showing "exited with code 1" because the ERR trap never copied the last compiler or module-not-found line from the deploy log.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 17:39:03 +03:30
14 changed files with 548 additions and 23 deletions
+2 -1
View File
@@ -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` 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` - 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 - 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` - Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods`
### User products (customer listings) ### User products (customer listings)
@@ -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}" "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", "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": { "request": {
"method": "POST", "method": "POST",
"header": [ "header": [
@@ -1754,11 +1811,11 @@
] ]
}, },
"url": "{{baseUrl}}/businesses/{{businessId}}/payments/mellat/callback", "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": { "request": {
"method": "GET", "method": "GET",
"header": [], "header": [],
@@ -1777,7 +1834,7 @@
{ "key": "Status", "value": "OK" } { "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."
} }
} }
] ]
+93 -2
View File
@@ -3210,7 +3210,8 @@
"tags": [ "tags": [
"Payments" "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": [ "parameters": [
{ {
"$ref": "#/components/parameters/businessId" "$ref": "#/components/parameters/businessId"
@@ -3255,7 +3256,7 @@
"tags": [ "tags": [
"Payments" "Payments"
], ],
"summary": "Bank payment callback (GET variant)", "summary": "Bank payment callback GET — legacy direct API URL",
"parameters": [ "parameters": [
{ {
"$ref": "#/components/parameters/businessId" "$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"
}
}
}
} }
} }
} }
+1
View File
@@ -11,6 +11,7 @@
# #
# Scripts on the VM (same folder): # Scripts on the VM (same folder):
# patch-nginx-seo.sh <host> — add /sitemap.xml + /robots.txt API proxies to an existing nginx site # patch-nginx-seo.sh <host> — add /sitemap.xml + /robots.txt API proxies to an existing nginx site
# patch-nginx-pay.sh <host> — add /meshkee/payments/{zarinpal|mellat}/callback API proxies
# #
# Env (.env): PORT, DEPLOY_TOKEN, ALLOWED_SLUGS # Env (.env): PORT, DEPLOY_TOKEN, ALLOWED_SLUGS
# #
+34 -1
View File
@@ -28,12 +28,45 @@ write_status() {
>"$STATUS" >"$STATUS"
} }
fail_snippet() {
if [[ ! -f "$LOG" ]]; then
return 0
fi
local chunk snippet=""
# Only the current deploy (log is appended across runs).
chunk="$(awk 'BEGIN{s=""} /^==== / && /deploy start:/{s=""} {s=s $0 ORS} END{printf "%s", s}' "$LOG")"
snippet="$(
{ printf '%s' "$chunk" | grep -E "Error:|Type error|Module not found|ELIFECYCLE|npm ERR!" || true; } \
| sed 's/\x1b\[[0-9;]*m//g' \
| tail -n 4 \
| tr '\n' ' ' \
| sed 's/[[:space:]]\+/ /g'
)"
if [[ -z "${snippet// }" ]]; then
snippet="$(
printf '%s' "$chunk" \
| tail -n 12 \
| sed 's/\x1b\[[0-9;]*m//g' \
| tr '\n' ' ' \
| sed 's/[[:space:]]\+/ /g'
)"
fi
printf '%s' "$snippet"
}
echo "==== $(date -u +%Y-%m-%dT%H:%M:%SZ) deploy start: $SLUG ====" echo "==== $(date -u +%Y-%m-%dT%H:%M:%SZ) deploy start: $SLUG ===="
write_status "started" "Deploy running" write_status "started" "Deploy running"
cleanup_fail() { cleanup_fail() {
local code=$? local code=$?
write_status "failed" "deploy.sh exited with code $code" trap - ERR
local snippet
snippet="$(fail_snippet)"
if [[ -n "${snippet// }" ]]; then
write_status "failed" "exit $code: $snippet"
else
write_status "failed" "deploy.sh exited with code $code"
fi
exit "$code" exit "$code"
} }
trap cleanup_fail ERR trap cleanup_fail ERR
+68
View File
@@ -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 <host>" >&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 <<NGINX
location = ${path} {
proxy_pass https://${API_HOST}/api/v1/tenants/${HOST}/payments/${gateway}/callback;
proxy_set_header Host ${API_HOST};
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;
}
NGINX
}
ensure_location() {
local gateway="$1"
local path="/meshkee/payments/${gateway}/callback"
if grep -qF "location = ${path}" "$NGINX_AVAILABLE"; then
echo "already present: ${path}"
return 0
fi
local block
block="$(proxy_location "$gateway")"
local tmp
tmp="$(mktemp)"
awk -v block="$block" '
/location \/ \{/ && !done {
print block
done = 1
}
{ print }
' "$NGINX_AVAILABLE" >"$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"
+25
View File
@@ -162,6 +162,28 @@ server {
add_header Cache-Control "public, max-age=900"; 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 / { location / {
proxy_pass http://127.0.0.1:${PORT}; proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -185,6 +207,9 @@ else
if [[ -x "$SCRIPT_DIR/patch-nginx-seo.sh" ]]; then if [[ -x "$SCRIPT_DIR/patch-nginx-seo.sh" ]]; then
"$SCRIPT_DIR/patch-nginx-seo.sh" "$HOST" || true "$SCRIPT_DIR/patch-nginx-seo.sh" "$HOST" || true
fi fi
if [[ -x "$SCRIPT_DIR/patch-nginx-pay.sh" ]]; then
"$SCRIPT_DIR/patch-nginx-pay.sh" "$HOST" || true
fi
fi fi
# SSL is issued separately via ssl.sh / Super Admin "Issue SSL" — do not run # SSL is issued separately via ssl.sh / Super Admin "Issue SSL" — do not run
+12 -2
View File
@@ -86,11 +86,21 @@ export class ZarinpalGateway implements PaymentGatewayAdapter {
const code = body.data?.code; const code = body.data?.code;
const authority = body.data?.authority?.trim() ?? ''; const authority = body.data?.authority?.trim() ?? '';
if (code !== ZARINPAL_OK || !authority) { 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( 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( throw new BadGatewayException(
`ZarinPal payment init failed (code ${code ?? 'unknown'})`, errorMsg
? `ZarinPal: ${errorMsg} (code ${code ?? 'unknown'})`
: `ZarinPal payment init failed (code ${code ?? 'unknown'})`,
); );
} }
+4 -2
View File
@@ -1,14 +1,16 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BusinessSettingsModule } from '../business-settings/business-settings.module'; import { BusinessSettingsModule } from '../business-settings/business-settings.module';
import { TenantModule } from '../tenant/tenant.module';
import { MellatGateway } from './gateways/mellat.gateway'; import { MellatGateway } from './gateways/mellat.gateway';
import { ZarinpalGateway } from './gateways/zarinpal.gateway'; import { ZarinpalGateway } from './gateways/zarinpal.gateway';
import { PaymentGatewayRegistry } from './payment-gateway.registry'; import { PaymentGatewayRegistry } from './payment-gateway.registry';
import { PaymentsController } from './payments.controller'; import { PaymentsController } from './payments.controller';
import { PaymentsService } from './payments.service'; import { PaymentsService } from './payments.service';
import { TenantPaymentsController } from './tenant-payments.controller';
@Module({ @Module({
imports: [BusinessSettingsModule], imports: [BusinessSettingsModule, TenantModule],
controllers: [PaymentsController], controllers: [PaymentsController, TenantPaymentsController],
providers: [ providers: [
MellatGateway, MellatGateway,
ZarinpalGateway, ZarinpalGateway,
+33 -2
View File
@@ -143,7 +143,7 @@ export class PaymentsService {
} }
const adapter = this.gateways.get(input.gatewayType); const adapter = this.gateways.get(input.gatewayType);
const callbackUrl = this.buildCallbackUrl( const callbackUrl = await this.buildCallbackUrl(
input.businessId, input.businessId,
input.gatewayType, 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 = const base =
this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') || this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') ||
`http://localhost:${this.config.get<string>('PORT') || '3000'}`; `http://localhost:${this.config.get<string>('PORT') || '3000'}`;
@@ -328,6 +337,28 @@ export class PaymentsService {
return `${base}/api/v1/businesses/${businessId.toString()}/payments/${gatewayType}/callback`; 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<string | null> {
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( private async findCallbackTransaction(
businessId: bigint, businessId: bigint,
gatewayType: string, gatewayType: string,
@@ -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<string, unknown>,
@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<string, unknown>,
@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);
}
}
+2 -1
View File
@@ -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` 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` - 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 - 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` - Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods`
### User products (customer listings) ### User products (customer listings)
@@ -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}" "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", "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": { "request": {
"method": "POST", "method": "POST",
"header": [ "header": [
@@ -1754,11 +1811,11 @@
] ]
}, },
"url": "{{baseUrl}}/businesses/{{businessId}}/payments/mellat/callback", "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": { "request": {
"method": "GET", "method": "GET",
"header": [], "header": [],
@@ -1777,7 +1834,7 @@
{ "key": "Status", "value": "OK" } { "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."
} }
} }
] ]
+93 -2
View File
@@ -3210,7 +3210,8 @@
"tags": [ "tags": [
"Payments" "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": [ "parameters": [
{ {
"$ref": "#/components/parameters/businessId" "$ref": "#/components/parameters/businessId"
@@ -3255,7 +3256,7 @@
"tags": [ "tags": [
"Payments" "Payments"
], ],
"summary": "Bank payment callback (GET variant)", "summary": "Bank payment callback GET — legacy direct API URL",
"parameters": [ "parameters": [
{ {
"$ref": "#/components/parameters/businessId" "$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"
}
}
}
} }
} }
} }