Payment Mapping — Nayax → Saipos¶
TL;DR: Convertemos eventos de pagamento da Nayax em SaiposOrderDto, mapeando tipos/códigos de pagamento, normalizando valores, gerando IDs válidos, equilibrando somatórios e persistindo tudo antes de enfileirar o outbox job.
Escopo¶
- Entrada: Payload de transação Nayax (
ev) - Saída:
SaiposOrderDtopronto para envio ao Saipos - Uso: Pipeline de integração (webhook Nayax → serviço → fila Saipos)
- Arquivo:
reference/payment-mapping.md
Tipos & Contratos¶
import { SaiposOrderDto } from '../dto/nayax.dto';
type SaiposType = 'ONLINE' | 'OFFLINE';
type SaiposCode = 'PARTNER_PAYMENT' | 'DIN' | 'DEB' | 'CARD' | 'CRE' | 'VALE' | 'OTHER';
interface SaiposPaymentOut {
code: SaiposCode;
amount: number;
change_for: number;
complement: string;
type: SaiposType;
}
Tabela de Mapeamento (TENDER_MAP)¶
Nayax tenderType | Saipos code | type | complement | Observações |
|---|---|---|---|---|
| 1 | DIN | OFFLINE | — | Dinheiro |
| 2 | CRE | OFFLINE | "" | Cartão genérico |
| 3 | OTHER | OFFLINE | — | "Cash Change" |
| 8 | DEB | OFFLINE | — | Débito |
| 14 | CRE | OFFLINE | — | Amex |
| 16 | CRE | OFFLINE | — | Visa |
| 18 | CRE | OFFLINE | — | Diners |
| 28 | CRE | OFFLINE | — | Mastercard |
| 23 | VALE | OFFLINE | — | Voucher |
| 37 | PARTNER_PAYMENT | ONLINE | "" | Transferência |
| 50 | PARTNER_PAYMENT | ONLINE | "pix" | Pix |
| 4 | null | — | — | Rounding (ignorado) |
| 5 | null | — | — | Cupom (tratado em desconto) |
| 7 | null | — | — | Gorjeta (ignorada no total) |
Heurística de Fallback¶
- Texto contém cash/dinheiro →
DIN/OFFLINE - pix/online/qr →
PARTNER_PAYMENT/ONLINE (complement"pix"se houver “pix”) - debit/débito/immediate →
DEB/OFFLINE - credit/crédito/visa/master/amex/diners →
CRE/OFFLINE - Caso contrário →
OTHER/OFFLINE
Regras de ID & Display¶
order_id: máx 30 caracteresdisplay_id: máx 15 caracteres- Transação de teste (
isTestTransactionoustoreCode === 'STORE1001'): - Sufixa
-<epoch>curto - Trunca base conforme espaço disponível
Itens, Modificadores e Totais¶
- Cada item:
integration_code←itemCodedesc_item←itemNameunit_price←priceouamountquantity← númerochoice_itemsa partir demodifiers:integration_code,desc_item_choice,aditional_price,quantity
- Gross dos itens = soma de (preço × qty) + modificadores
- Desconto = soma absoluta de
coupons[].couponSum - total_increase =
total_amount− (itemsGross−total_discount) se positivo; senão 0 - Arredondamento: função
to2(2 casas decimais)
Pagamentos & Balanceamento¶
- Constrói
payment_typesa partir deev.paymentsaplicandomapPaymentByTender. - Ignora pagamentos sem valor ou inválidos.
- Se vazio, injeta 1 linha
OTHER/OFFLINEcobrindototal_amount. - Balanceamento:
- Ajusta o último pagamento para fechar diferença (
target - sumBefore). - Se ficar negativo, redistribui consumindo anteriores.
- Corrige “poeira” de
< 0.01.
Saída (SaiposOrderDto)¶
{
"order_id": "5417-STORE1001-...-1761746662",
"display_id": "123456789-17617",
"cod_store": "COD_STORE_SAIPOS",
"created_at": "2025-10-29T14:04:05Z",
"notes": "Nayax pos=POS001 store=STORE1001",
"total_increase": 45,
"total_discount": 0,
"total_amount": 100,
"customer": {
"id": "5417",
"name": "Cliente",
"phone": ""
},
"order_method": {
"mode": "TICKET",
"scheduled": false,
"delivery_date_time": "2025-10-29T14:04:05Z"
},
"items": [
{
"integration_code": "ITEM001",
"desc_item": "Test Item Name",
"quantity": 1,
"unit_price": 50,
"notes": "",
"choice_items": [
{
"integration_code": "MOD001",
"desc_item_choice": "Extra Cheese",
"aditional_price": 5,
"quantity": 1,
"notes": ""
}
]
}
],
"payment_types": [
{
"code": "PARTNER_PAYMENT",
"amount": 100,
"change_for": 0,
"type": "ONLINE",
"complement": "pix"
}
]
}
Funções-Chave¶
- Normaliza datas com
toISOString()sem milissegundos - Trata
isTestTransaction/STORE1001 - Monta itens/modificadores
- Calcula
total_discount,total_increase - Gera e balanceia
payment_types
Ingestão & Persistência (NayaxService)¶
@Injectable()
export class NayaxService {
async ingestAndEnqueue(dto: any, actor: { id: string; email: string } | null = null)
}
Regras de negócio críticas:
- Auth obrigatória; ator precisa ter vínculo
userRestaurantcom role=OWNER - Idempotência por
transactionKey - Teste (
STORE1001/isTestTransaction) →transactionKeyúnico com-Date.now() - Se já existir:
- Teste → ignora duplicata
- Produção →
400 BadRequest(“Transação já processada”)
- Persistência:
nayaxEvent(cabeçalho +rawPayload)nayaxItem+nayaxModifier+nayaxTaxInfo+nayaxPromotionnayaxTaxInvoicenayaxEmployeenayaxPayment(com parsing: números, datas,maskLast4, etc.)- Outbox:
- Cria
outboxJob(action=CREATEsetransactionType===1, senãoCANCEL) - Trata
P2002(job já existe) com log informativo
Regras de Data & Hora¶
created_at/delivery_date_time→transactionDateem ISO (sem milissegundos)transactionStartDateTime/transactionEndDateTimepersistidos quando presentesparseExpireDateaceita formatos: ISO,MM/YY,MM/YYYY,YYYY-MM
Edge Cases & Decisões de Design¶
- Sem pagamentos → 1 linha
OTHER/OFFLINEcobrindo 100% do valor - Cupom/Gorjeta/Rounding:
- Cupom: entra no desconto
- Gorjeta/Rounding: não alteram
payment_types; rounding (tender 4) é ignorado - Poeira de centavos (< 0,01) corrigida no último pagamento
- Telefone do cliente: apenas dígitos (
replace(/\D/g, '')) - IDs truncados antes do sufixo de teste (quando necessário)
Exemplo de Uso (pseudo-pipeline)¶
// controller
@Post('/webhooks/nayax')
async receive(@Body() payload: any, @Req() req: any) {
const actor = req.user; // precisa estar autenticado
await this.nayaxService.ingestAndEnqueue(payload, actor);
return { ok: true };
}
// worker (later)
const order = mapNayaxToSaipos(nayaxEvent.rawPayload, store.codStoreSaipos);
await saiposClient.createOrder(order);
Checklist de Qualidade¶
-
order_id≤ 30 edisplay_id≤ 15 (incluindo sufixo de teste) -
payment_typessoma ==total_amount(±0,01 fixado) -
total_increasecoerente com gross − desconto - Nenhum pagamento com
amount <= 0 -
isTestTransactionrespeitado (duplicatas ignoradas) - Outbox criado uma única vez (idempotência)
-
phonesomente dígitos - Datas ISO sem milissegundos
Troubleshooting¶
- Valores não fecham → valide
itemsGross,total_discount,total_amount; verifique se gorjetas/rounding vieram como itens e não empayments. - Pix virou OTHER → cheque se
tenderType= 50 ou se o texto contém “pix/qr/online”. - Duplicata em produção → confirme
transactionKeyetransactionType. - Cartão sem bandeira → cai em
CREgenérico (ok por design).
Changelog¶
- v1.0.0: Mapeamento inicial, heurística de fallback, balanceamento de pagamentos, idempotência e outbox.
Referência Rápida (snippets)¶
mapPaymentByTender¶
function mapPaymentByTender(
p: any,
): { code: SaiposCode; type: SaiposType; complement: string } | null
- Retorna
nullpara tenderTypes ignorados (4,5,7) - Aplica heurística textual quando necessário