Repository navigation
fix(apps): Stop duplicating orders exported to Tiny and Bling ERP #877
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,25 @@ | ||
| import type { Orders } from '@cloudcommerce/types'; | ||
| import { logger } from '@cloudcommerce/firebase/lib/config'; | ||
| import { acquireExportLock } from '@cloudcommerce/firebase/lib/helpers/export-lock'; | ||
| import api from '@cloudcommerce/api'; | ||
| import postTiny from './post-tiny-erp'; | ||
| import parseStatusToTiny from './parsers/status-to-tiny'; | ||
| import parseTinyStatus from './parsers/status-from-tiny'; | ||
| import parseOrder from './parsers/order-to-tiny'; | ||
|
|
||
| const EXPORT_LOCKS_COLLECTION = 'tinyErpExportLocks'; | ||
| /* Longer than anything holding the lock can live: the events function times out | ||
| in 60s, and the cron holds it for a single order (the POST times out in 30s). */ | ||
| const EXPORT_LOCK_STALE_MS = 1000 * 60 * 2; | ||
| const CONCURRENT_EXPORT_WAIT_MS = 20000; | ||
|
|
||
| /* `pedido.incluir.php` returns the created order in `registros.registro` */ | ||
| const getCreatedTinyId = (response: Record<string, any>) => { | ||
| const { registros } = response; | ||
| const registro = (Array.isArray(registros) ? registros[0] : registros)?.registro; | ||
| return registro?.id as string | number | undefined; | ||
| }; | ||
|
|
||
| export default async (apiDoc, queueEntry, appData, canCreateNew) => { | ||
| const orderId = queueEntry.nextId; | ||
| let order: Orders; | ||
|
|
@@ -52,39 +66,38 @@ export default async (apiDoc, queueEntry, appData, canCreateNew) => { | |
| logger.info(`${orderId} skipped with last fulfillment status from Tiny`); | ||
| return null; | ||
| } | ||
| logger.info(`${orderId} searching order ${order.number}`); | ||
|
|
||
| let tinyData: { pedidos?: any }; | ||
| try { | ||
| tinyData = await postTiny('/pedidos.pesquisa.php', { | ||
| numeroEcommerce: String(order.number), | ||
| }); | ||
| } catch (err: any) { | ||
| const status = err.response && err.response.status; | ||
| if (status === 404) { | ||
| tinyData = {}; | ||
| } else { | ||
| logger.info(`${orderId} search on tiny ends with status ${status}`); | ||
| if (!status) { | ||
| logger.error(err); | ||
| const searchTinyOrder = async () => { | ||
| logger.info(`${orderId} searching order ${order.number}`); | ||
| let tinyData: { pedidos?: any }; | ||
| try { | ||
| tinyData = await postTiny('/pedidos.pesquisa.php', { | ||
| numeroEcommerce: String(order.number), | ||
| }); | ||
| } catch (err: any) { | ||
| const status = err.response && err.response.status; | ||
| if (status === 404) { | ||
| tinyData = {}; | ||
| } else { | ||
| logger.info(`${orderId} search on tiny ends with status ${status}`); | ||
| if (!status) { | ||
| logger.error(err); | ||
| } | ||
| throw err; | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
| const { pedidos } = tinyData; | ||
| let originalTinyOrder: undefined | Record<string, any>; | ||
| if (Array.isArray(pedidos)) { | ||
| originalTinyOrder = pedidos | ||
| const { pedidos } = tinyData; | ||
| if (!Array.isArray(pedidos)) return undefined; | ||
| const foundEntry = pedidos | ||
| .sort((a: { pedido: Record<string, any> }, b: { pedido: Record<string, any> }) => { | ||
| return a.pedido.id < b.pedido.id ? 1 : -1; | ||
| }) | ||
| .find(({ pedido }) => { | ||
| return order.number === Number(pedido.numero_ecommerce); | ||
| }); | ||
| if (originalTinyOrder) { | ||
| originalTinyOrder = originalTinyOrder.pedido; | ||
| } | ||
| } | ||
| return foundEntry?.pedido as undefined | Record<string, any>; | ||
| }; | ||
|
|
||
| let originalTinyOrder = await searchTinyOrder(); | ||
|
|
||
| if (!originalTinyOrder) { | ||
| if (!canCreateNew) { | ||
|
|
@@ -113,24 +126,52 @@ export default async (apiDoc, queueEntry, appData, canCreateNew) => { | |
| break; | ||
| } | ||
| } | ||
| const tinyOrder = await parseOrder(order, appData); | ||
| try { | ||
| logger.info(`Posting ${orderId}`, { tinyOrder }); | ||
| } catch { | ||
| logger.info(`Posting ${orderId} (stringify failed)`); | ||
| } | ||
| return postTiny('/pedido.incluir.php', { | ||
| pedido: { | ||
| pedido: tinyOrder, | ||
| }, | ||
| }).then((response) => { | ||
| const exportLock = await acquireExportLock(EXPORT_LOCKS_COLLECTION, orderId, { | ||
| staleLockMs: EXPORT_LOCK_STALE_MS, | ||
| maxWaitMs: queueEntry.concurrentExportWaitMs ?? CONCURRENT_EXPORT_WAIT_MS, | ||
| }); | ||
| if (!exportLock.isLocked) { | ||
| /* Created by a concurrent execution while this one waited, the search above | ||
| could not see it: falls through to the status update on that order. */ | ||
| const { exported } = exportLock; | ||
| originalTinyOrder = exported.id | ||
| ? { id: exported.id, situacao: exported.situacao } | ||
| : await searchTinyOrder(); | ||
| if (!originalTinyOrder) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quem perde o lock desiste quando o POST do vencedor falha. O perdedor procura o pedido uma única vez, 5 s depois. Se o POST do vencedor falha (código 6 do Tiny, que vira 503, ou 5xx do Bling), o vencedor libera o lock e lança erro. Só que o perdedor já saiu, e evento de pedido é Resultado: nenhuma das duas execuções cria o pedido. No Tiny ele só volta pelo cron de 3 em 3 h, e só se estiver pago. No Bling ele não volta. Sugestão: o perdedor deve acompanhar o estado do lock, e não do ERP. Se o vencedor liberar o lock, o perdedor assume e cria o pedido. |
||
| logger.warn(`${orderId} exported by a concurrent execution but not found`); | ||
| return null; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reexportação manual pulada vira sucesso. Na fila manual ( Isso acontece quando o lojista apaga o pedido no ERP e reenfileira, ou quando uma tentativa anterior morreu com o lock preso. O painel mostra sucesso e nada foi criado. Aqui o retorno precisa ser um erro. |
||
| } | ||
| } else { | ||
| let tinyOrder: Record<string, any>; | ||
| let response: Record<string, any>; | ||
| try { | ||
| tinyOrder = await parseOrder(order, appData); | ||
| try { | ||
| logger.info(`Posting ${orderId}`, { tinyOrder }); | ||
| } catch { | ||
| logger.info(`Posting ${orderId} (stringify failed)`); | ||
| } | ||
| exportLock.setSending(); | ||
| response = await postTiny('/pedido.incluir.php', { | ||
| pedido: { | ||
| pedido: tinyOrder, | ||
| }, | ||
| }); | ||
| } catch (err) { | ||
| await exportLock.fail(err); | ||
| throw err; | ||
| } | ||
| logger.info(`Created ${orderId} on Tiny`, { response }); | ||
| await exportLock.complete({ | ||
| id: getCreatedTinyId(response), | ||
| situacao: tinyOrder.situacao, | ||
| }); | ||
| const tinyErpOnNewOrder = global.$tinyErpOnNewOrder; | ||
| if (tinyErpOnNewOrder && typeof tinyErpOnNewOrder === 'function') { | ||
| return tinyErpOnNewOrder({ response, order, postTiny }); | ||
| } | ||
| return response; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| if (originalTinyOrder) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sem teste automatizado do lock. A validação no emulador não foi commitada, e os 84 testes do
bling-erpnão passam por nenhuma linha nova. Os caminhos de takeover, liberação e espera do perdedor são justamente os que quase nunca rodam em produção.