Playbook: Pinbox24 Mailgun — Duplicate Check & File Metadata Fix

Incident date: 2026-07-02 Resolved: 2026-07-02 — permanent fix via Docker bind mounts (runtime) + GitLab MRs (code source) Severity: P1 — all inbound emails landed at processStatusId: 300 (duplicates to confirm), blocking AI processing pipeline Services affected: mailgun-prod_mailgun_1, s3-v2-v42-prod on bms-1 (94.23.26.113) GH tracking: #2048 GitLab MRs (code-level permanent fix):


Symptoms

  • Every inbound email to rechnung-AI@integrations-eu.pinbox24.com created a register record in the AI-Logs register
  • All records had processStatusId: 300 | duplicates to confirm and duplicate: Y
  • recordData.originalName, recordData.fileSize, recordData.recordMainDocument were all MISSING on every record
  • Activity 296 (filter_by_mq_filter_and_update) ran originalName='##originalName##' AND fileSize='##fileSize##' with empty substitutions → found 0 records → duplicateCount: 0 → activity 297 routed to 299 → 300

Root Cause

Two separate bugs combined to break the flow:

Bug 1 — s3-v2 GET vs POST method mismatch

The mailgunFileHandler route in s3-v2-v42-prod was registered as GET but p4-ms-mailgun calls it with POST. All requests received 404. The GET→POST routing fix had been applied at some earlier point (visible in logs: 404s before 15:20 UTC, 200s after).

Bug 2 — File-last ordering: metadata never reached the register record

The original integration.helper.js flow in mailgun-prod_mailgun_1:

1. Build doc (email headers, no file metadata)
2. POST /api/reg/:regid/new  ← register record created WITHOUT originalName/fileSize/recordMainDocument
3. uploadMailgunAttachments(attachments, registerRecord.result, ...)  ← file upload, return value IGNORED

originalName, fileSize, recordMainDocument were never set on doc before record creation.

Bug 3 — s3-v2 response wrapper not unwrapped

When the s3-v2 patch was applied to return file metadata, the raw response is wrapped:

{ "success": true, "result": { "_id": "...", "originalName": "...", "size": 12345 } }

The initial mailgun patch checked fileResult._id directly — always undefined. The unwrapped value is at fileResult.result._id.


Fix Applied (runtime hotfix — 2026-07-02)

Patch 1: s3-v2-v42-prod — return file metadata instead of string

File: /app/dist/apps/storage/mailgunFileHandler.helper.js

Applied via patch_s3v2.js (Node.js patcher):

  1. Capture Promise.all result: const results = yield Promise.all(...)
  2. After upload: return { _id: doc._id, originalName: fileData.originalname, size: attachment.size };
  3. Return first result: return results.filter(Boolean)[0] || null; (was: return "All files save";)

Verification: content-length changed from 42 bytes ({"success":true,"result":"All files save"}) to ~117 bytes (JSON object with _id, originalName, size).

Patch 2: mailgun-prod_mailgun_1 — file-first ordering with correct response unwrapping

File: /app/helper/integration.helper.js

Applied via patch_mailgun_v3.js (Node.js patcher) — replaced the registerinsert block:

if (action.type == "registerinsert") {
    // PATCH v3: upload file FIRST; s3-v2 wraps response in { success, result }
    var fileResult = null;
    if (attachments) {
        var attach = JSON.parse(attachments);
        if (attach && attach.length) {
            var tempRecord = { officeId: config.officeId, regId: action.regData.regid };
            fileResult = await s3RequestHelper.uploadMailgunAttachments(attach, tempRecord, bodyMail.emailSender);
        }
    }
    var fr = (fileResult && fileResult.result) ? fileResult.result : fileResult;
    if (fr && fr._id) {
        doc.recordMainDocument = String(fr._id);
        doc.originalName = fr.originalName;
        doc.fileSize = String(fr.size);  // String() is required: mqFilter uses '##fileSize##' string comparison
    }
    var registerRecord = await requestHelper.makePostRequest(`/api/reg/${action.regData.regid}/new`, doc);
    assert((registerRecord && registerRecord.success && registerRecord.result), ...);
}

Key facts:

  • s3-v2 always wraps responses: { success: bool, result: <actual value> } (see storage.controller.js:109)
  • fileSize MUST be stored as string — the mqFilter fileSize='##fileSize##' uses string equality; MongoDB strict type matching fails if stored as number
  • tempRecord (no _id) is passed to s3-v2 for the file-first upload — recId: undefined is OK; s3-v2 uses it for the file doc but the register record is not yet created at that point

Verification

Real PDF from user (2026-07-02 17:57 UTC) — PROCEDURA OPERACYJNA - przeglądów technicznych ecotrans - W opracowaniu - v8.pdf (215,899 bytes):

processStatusId: 71 | Document passed to docRegistration  ✅
recordData.originalName: PROCEDURA OPERACYJNA...v8.pdf     ✅
recordData.fileSize: 215899                                 ✅
recordData.recordMainDocument: 6a468a6c09bbe50217ade025    ✅
recordData.duplicateCount: 1  (self-match only)             ✅
recordData.duplicate: N                                     ✅

Durability — Stan po sesji 2026-07-07

Patche przeżywają docker start i docker-compose up -d — bind mount w konfiguracji kontenera.

ContainerPatchBind mount (host→container)Przeżywa restart
mailgun-v42-prodintegration.helper.js (PATCH v3, 4×)/root/mailgun-prod/patches/integration.helper.js:/app/helper/integration.helper.js:rodocker startdocker-compose up -d
s3-v2-v42-prodmailgunFileHandler.helper.js/root/s3v2-prod/patches/mailgunFileHandler.helper.js:/app/dist/apps/storage/mailgunFileHandler.helper.js:rodocker startdocker-compose up -d

WAŻNE: Stara nazwa kontenera mailgun to mailgun-prod_mailgun_1. Od 2026-07-07 używaj mailgun-v42-prod. Playbook bms1-post-restart-recovery.md zaktualizowany — docker start mailgun-v42-prod.

MONGODB_URL w /root/mailgun-prod/mailgun-environment.env:

  • Używa tej samej wartości co NEW_MONGODB_URI w kontenerze v42-prod (env var z SOPS V42_NEW_MONGODB_URI)
  • Łączy się do w4_db na bms-2/bms-3 przez w4_app user — NIE używaj admin credentials (inny klucz SOPS!)
  • S3_V2_SERVER_URL=http://s3-v2-v42-prod:3000

integrationsEmails — konfiguracje EcoTrans:

  • rechnung-AI@integrations-eu.pinbox24.com — ✅ skonfigurowany (AI-Logs, officeId 5c752bbca20da35ca1b99083)
  • ai-standard-docs@integrations-eu.pinbox24.com — ✅ skonfigurowany
  • gutschrift_AI@integrations-eu.pinbox24.com — ✅ dodany 2026-07-07 (brak → insert do MongoDB w4_db)

Jeśli kontenery zostaną usunięte (docker rm + nowy obraz)

Bind mount musi być re-aplikowany. Opcje:

Opcja A — docker-compose up (zalecana dla mailguna):

cd /root/mailgun-prod
CONTAINER_NAME=mailgun-v42-prod IMAGE_NAME=mailgun-v42-prod:latest CLUSTER_NETWORK=test-net docker-compose up -d
# docker-compose.yml ma już volumes: bind mount → patch przeżywa

Opcja B — re-aplikuj patch ręcznie (gdy bind mount niedostępny):

# Copy patch scripts to bms-1
scp -i C:\Users\konar\.ssh\id_ed25519 docs\pinbox24\patch_s3v2.js root@94.23.26.113:/tmp/ # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
scp -i C:\Users\konar\.ssh\id_ed25519 docs\pinbox24\patch_mailgun_v3.js root@94.23.26.113:/tmp/ # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
 
# Apply s3-v2 patch
ssh -i C:\Users\konar\.ssh\id_ed25519 -o BatchMode=yes root@94.23.26.113 "docker cp /tmp/patch_s3v2.js s3-v2-v42-prod:/tmp/ && docker exec s3-v2-v42-prod node /tmp/patch_s3v2.js && docker exec s3-v2-v42-prod pm2 restart all --update-env" # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
 
# Apply mailgun patch
ssh -i C:\Users\konar\.ssh\id_ed25519 -o BatchMode=yes root@94.23.26.113 "docker cp /tmp/patch_mailgun_v3.js mailgun-v42-prod:/tmp/ && docker exec mailgun-v42-prod node /tmp/patch_mailgun_v3.js && docker exec mailgun-v42-prod pm2 restart all --update-env" # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
 
# Verify: check recent AI-Logs records have originalName/fileSize/recordMainDocument
# Send a test email and confirm processStatusId reaches 71, not 300

Permanent Fix Required

The patches must be committed to the GitLab TypeScript sources:

ServiceGitLab repoFile to change
pinbox24-ms-s3-v2gitlab.com/pinbox24/pinbox24-ms-s3-v2src/apps/storage/mailgunFileHandler.helper.ts — return { _id, originalName, size }
p4-ms-mailgungitlab.com/pinbox24/p4-ms-mailgunsrc/helper/integration.helper.ts — file-first flow, unwrap fileResult.result

Access: GITLAB_ADMIN_PAT in secrets/administration.env.sops.

GH issue to track: create with label bug, pinbox24, tech-debt and milestone permanent-fix.


Smoke Test After Deploy

Send a test email with datetime-unique filename and verify the record:

// Run inside v42-prod container: node /tmp/send_test_v4.js
// (script at docs/pinbox24/send_test_v4.js)
// Expected: record appears with processStatusId 71 (or 298 after duplicate check), NOT 300

Check record:

// Run inside v42-prod container (script at docs/pinbox24/check_all_recent.js)
// Look for: originalName, fileSize, recordMainDocument all populated; duplicateCount=1; duplicate=N

Activity Flow Reference

The AI-Logs register process (aiProcessingLog) handles inbound emails:

ActivityIDTypeAction
check duplicates296triggerfilter_by_mq_filter_and_update on regRecords using originalName='##originalName##' AND fileSize='##fileSize##' → sets duplicateCount
if potential duplicate297switchkey "1" → 298 (no dup); ##isEmpty##/##isNotEmpty## other → 299
no duplicate298triggerupdate_register_record: sets duplicate: "N"
check if duplicate299triggerfurther verification
duplicates to confirm300statehuman review needed
Document passed to docRegistration71stateAI processing proceeds

duplicateCount = 1 is expected and correct for a new unique file — the record matches only itself.