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>
This commit is contained in:
co-authored by
Cursor
parent
40284df880
commit
e942348b1d
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#
|
||||
# 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-pay.sh <host> — add /meshkee/payments/{zarinpal|mellat}/callback API proxies
|
||||
#
|
||||
# Env (.env): PORT, DEPLOY_TOKEN, ALLOWED_SLUGS
|
||||
#
|
||||
|
||||
Executable
+68
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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'})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') ||
|
||||
`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`;
|
||||
}
|
||||
|
||||
/** 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(
|
||||
businessId: bigint,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user