Skip to content

(SP: 3)[Backend][Shop][Monobank] Webhook apply hardening + safer cleanup (archive test products)#305

Merged
ViktorSvertoka merged 57 commits into
developfrom
lso/feat/shop
Feb 11, 2026
Merged

(SP: 3)[Backend][Shop][Monobank] Webhook apply hardening + safer cleanup (archive test products)#305
ViktorSvertoka merged 57 commits into
developfrom
lso/feat/shop

Conversation

@liudmylasovetovs
Copy link
Copy Markdown
Collaborator

@liudmylasovetovs liudmylasovetovs commented Feb 10, 2026

Description

This PR hardens Monobank webhook/apply behavior and improves test cleanup safety to avoid fragile hard-deletes against production-like FK constraints. Large refactors are intentionally deferred to reduce merge risk.


Related Issue

Issue: #<issue_number>


Changes

  • Preserve the original Monobank snapshot validation error (do not overwrite it with cancel-order failures); cancel errors are logged only.
  • Adjust Monobank webhook “paid-terminal” guard to not swallow reversed/failure/expired events (they must reach their handlers); keep guard only for informational/duplicate cases.
  • Replace raw-SQL status filters with typed Drizzle inArray(...) in Monobank order/attempt queries (no behavior change intended).
  • Remove an extra DB round-trip for attemptNumber by returning/passing it from the existing attempt row / newly created attempt.
  • Tests: update Monobank PSP_UNAVAILABLE test cleanup to archive test products instead of hard-deleting when FK constraints block deletion.

Database Changes (if applicable)

  • Schema migration required
  • Seed data updated
  • Breaking changes to existing queries
  • Transaction-safe migration
  • Migration tested locally on Neon

How Has This Been Tested?

  • Tested locally

PowerShell:

  • npx vitest lib/tests/shop/monobank-*.test.ts
  • npx vitest lib/tests/shop/*webhook*.test.ts
  • npx vitest run lib/tests/shop
  • npm run build

Screenshots (if applicable)

N/A


Checklist

Before submitting

  • Code has been self-reviewed
  • No TypeScript or console errors
  • Code follows project conventions
  • Scope is limited to this feature/fix
  • No unrelated refactors included
  • English used in code, commits, and docs
  • New dependencies discussed with team
  • Database migration tested locally (if applicable)
  • GitHub Projects card moved to In Review

Reviewers

Summary by CodeRabbit

  • Improvements

    • Monobank webhook processing reorganized for more reliable payment state handling, metadata merging, and automatic restocking flows
    • Payment attempt responses now include attemptNumber for clearer transaction tracking
  • Bug Fixes

    • Better detection and reporting of webhook issues (amount mismatches, out-of-order events, blocked transitions) with explicit error codes
  • Tests

    • Expanded test coverage for paid reversals, webhook outcome paths, and state-transition scenarios

…t dedupe + claim/lease TTL, paid terminal, mismatch→needs_review)
@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 10, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit b9a58a6
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/698bdacf04332c0008550cce
😎 Deploy Preview https://deploy-preview-305--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devlovers-net Ready Ready Preview, Comment Feb 11, 2026 1:27am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Refactors Monobank webhook processing into typed, modular helpers (fetching attempts/orders, status transitions, metadata merging, atomic DB updates, restock handling) and propagates attemptNumber through monobank attempt creation return values. Adds UUID utilities and multiple new unit tests validating webhook outcomes and paid-reversal flows.

Changes

Cohort / File(s) Summary
Webhook Processing Refactor
frontend/lib/services/orders/monobank-webhook.ts
Introduced types (AttemptRow, OrderRow, PaymentStatusTarget), many helper functions (fetchAttemptForWebhook, fetchOrderForAttempt, computeNextProviderModifiedAt, transitionPaymentStatus, buildMergedMetaSql, persistEventOutcome, atomicMarkPaidOrderAndSucceedAttempt, atomicFinalizeOrderAndAttempt, applyWebhookToOrderAttemptEvent, etc.), centralized provider_modified_at and metadata merging, added atomic multi-row updates and restock failure handling, replaced monolithic webhook logic with orchestrated workflow.
Attempt Creation Optimization
frontend/lib/services/orders/monobank.ts
Switched status checks to inArray from drizzle-orm and propagated attemptNumber through createMonoAttemptAndInvoiceImpl, createMonoAttemptAndInvoice, and createMonobankAttemptAndInvoice return shapes; removed redundant post-query.
UUID Utility
frontend/lib/utils/uuid.ts
Added UUID_V1_V5_RE and isUuidV1toV5 type guard for UUID v1/v5 validation.
Tests — Webhook Outcomes & Paid Reversal
frontend/lib/tests/shop/monobank-webhook-apply-outcomes.test.ts, frontend/lib/tests/shop/monobank-webhook-paid-reversal.test.ts, frontend/lib/tests/shop/monobank-webhook-apply.test.ts
Added comprehensive outcome tests and paid-reversal test, updated expectations to include appliedErrorCode, adjusted a test title/outcome to reflect blocked transition, and added assertions for persisted event fields.
Tests — PSP Unavailable / Cleanup
frontend/lib/tests/shop/monobank-psp-unavailable.test.ts
Replaced cleanupProduct with archiveProduct, added UUID validation in cleanup helpers, updated cleanupOrder to validate UUIDs and perform explicit deletions.
Tests — Stripe fixture helper
frontend/lib/tests/shop/stripe-webhook-mismatch.test.ts
Replaced product-picking helper with createStripeOrderFixture and safeCurrencyLiteral, simplified test fixture creation and currency normalization.

Sequence Diagram(s)

sequenceDiagram
    participant Receiver as Webhook Receiver
    participant DB as Database
    participant PaymentState as Payment State Service
    participant Restock as Restock Worker

    Receiver->>DB: readDbRows(invoiceId, referenceAttemptId)
    activate DB
    DB-->>Receiver: AttemptRow, OrderRow / null
    deactivate DB

    Receiver->>Receiver: computeNextProviderModifiedAt(provider_modified_at)
    Receiver->>Receiver: buildMergedMetaSql(normalized)
    Receiver->>Receiver: validate amount & status

    alt Transition requires payment-state change
        Receiver->>PaymentState: transitionPaymentStatus(orderId, to)
        activate PaymentState
        PaymentState-->>Receiver: {ok, applied, reason}
        deactivate PaymentState
    end

    Receiver->>DB: persistEventOutcome(eventId, appliedResult, mergedMeta)
    activate DB
    DB-->>Receiver: persisted
    deactivate DB

    alt Applied result is paid or finalized
        Receiver->>DB: atomicMarkPaidOrderAndSucceedAttempt / atomicFinalizeOrderAndAttempt
        activate DB
        DB-->>Receiver: success/failure
        deactivate DB

        opt restock needed
            Receiver->>Restock: restockOrder(orderId, reason, workerId)
            activate Restock
            Restock-->>Receiver: restock result
            deactivate Restock
        end
    end

    Receiver-->>Receiver: buildApplyOutcome() -> response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 I hopped through handlers, neat and bright,
Split the logic, set states right.
UUIDs checked, events logged fair,
Restock called with gentle care.
Tests now dance — the burrow's light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: Monobank webhook hardening, safer cleanup practices, and test product archiving rather than deletion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lso/feat/shop

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
frontend/lib/tests/shop/stripe-webhook-mismatch.test.ts (2)

63-63: Avoid sql.raw() for values — use parameterized interpolation instead.

sql.raw(...) bypasses query parameterization. Even though safeCurrencyLiteral currently constrains the value, this pattern is fragile if the utility is ever relaxed or reused. Drizzle's sql template literal supports safe interpolation with a cast:

Suggested fix
-      ${sql.raw(`'${currency}'`)},
+      ${currency}::currency,

209-214: Test cleanup may be skipped if an assertion fails earlier.

If any expect between lines 130–207 throws, the delete on line 209 never executes and the test row remains in the DB. Since the ID is a random UUID this is low-risk, but wrapping the body in try/finally (or moving cleanup to afterEach with a captured orderId) would make it more robust.

frontend/lib/services/orders/monobank-webhook.ts (3)

91-96: readDbRows is duplicated in the test file.

This utility is copy-pasted into frontend/lib/tests/shop/monobank-webhook-apply-outcomes.test.ts (lines 8-13). Consider extracting it to a shared utility (e.g., @/lib/utils/db or similar) to keep a single source of truth.


620-634: Repeated pattern: transition + separate order update could be consolidated.

In both the mismatch handler (lines 613-634) and the out-of-order success handler (lines 699-719), after transitionPaymentStatus succeeds, a follow-up orders update sets failureCode/failureMessage. This could be folded into guardedPaymentStatusUpdate's set parameter to avoid the extra round-trip and partial-write window. Low priority given these are error/edge-case paths.

Also applies to: 706-719


311-337: SQL ORDER BY CASE gives correct priority but consider adding AND status NOT IN ('succeeded','failed','canceled') to avoid matching already-finalized attempts.

Currently fetchAttemptForWebhook will match any attempt for the given invoiceId regardless of attempt status. If a webhook arrives late and the attempt is already finalized (e.g., succeeded), the downstream logic handles it (noop paths), but filtering at the query level would be a clearer signal. This is optional — the status guards downstream are sufficient.

frontend/lib/tests/shop/monobank-webhook-apply-outcomes.test.ts (2)

8-13: readRows duplicates readDbRows from monobank-webhook.ts.

Same helper exists in the production code. Consider sharing it from a common utility module.


217-229: Cleanup does not guard against invalid UUIDs unlike cleanupOrder in the PSP test.

The cleanup function in monobank-psp-unavailable.test.ts validates UUIDs via isUuidV1toV5 before executing deletes. This test's cleanup doesn't, though since orderId and attemptId are generated via crypto.randomUUID() within the test, the risk is minimal.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87c1314ce0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +544 to +546
orderRow.payment_status === 'paid' &&
(status === 'success' || status === 'processing' || status === 'created')
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve success timestamp updates before paid no-op

In applyWebhookToMatchedOrderAttemptEvent, this paid-state guard now short-circuits status === 'success' to applied_noop, so the success path that updates payment_attempts.provider_modified_at no longer runs for duplicate/later success webhooks. Because out-of-order filtering earlier in the same function relies on attempt.provider_modified_at, a newer success timestamp can be dropped, letting an older reversed/expired event appear fresh and be applied, which can incorrectly move the order into refund/failure handling and trigger restock.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
frontend/lib/services/orders/monobank-webhook.ts (4)

41-62: Manual snake_case types mirror raw SQL results — maintenance risk.

AttemptRow and OrderRow use snake_case to match raw db.execute(sql\...`) results, which bypasses Drizzle's type inference. If a column is renamed or its type changes in the schema, these types silently drift. Consider using Drizzle's typed query builder (.select().from(...)) instead of raw SQL in fetchAttemptForWebhookandfetchOrderForAttempt, or at least derive these types from the schema (e.g., typeof paymentAttempts.$inferSelect`).


519-523: Redundant re-assignment of appliedResult.

At line 522, appliedResult = 'applied_with_issue' is identical to its initial value on line 485. Same for line 606 vs 583. The else branches are dead code — the value is already 'applied_with_issue'.

♻️ Remove redundant assignments
       } else {
-        appliedResult = 'applied_with_issue';
+        // transition blocked, appliedResult already 'applied_with_issue'
       }

Also applies to: 604-607


789-804: Unknown status events are logged but silently nooped.

Unknown/unrecognized Monobank statuses produce applied_noop with an UNKNOWN_STATUS error code persisted to the event. This is reasonable as a safe default, but consider whether these should surface in operational monitoring (e.g., an alert) so that newly introduced Monobank statuses don't go unnoticed.


304-324: Consider refactoring to Drizzle query builder if the complex ORDER BY logic can be simplified.

fetchAttemptForWebhook uses raw SQL with db.execute(sql\...`)and casts to{ rows?: AttemptRow[] }. While the cast is safe—both drivers in use (neon-http and node-postgres) reliably return a .rowsproperty—raw SQL here is justified by theORDER BY CASE` logic, which the Drizzle query builder cannot easily express. If the priority ordering can be simplified or moved to application logic, consider migrating to Drizzle's query builder for better type safety and consistency.

frontend/lib/tests/shop/monobank-psp-unavailable.test.ts (1)

80-84: Minor UUID regex inconsistency with monobank-webhook.ts.

This isUuid regex requires version nibble [1-5], while UUID_RE in monobank-webhook.ts (line 65) uses [0-9a-f] (any hex digit). Both work fine for crypto.randomUUID() (v4), but it's worth noting the inconsistency if you ever need to share a single utility.

frontend/lib/tests/shop/monobank-webhook-paid-reversal.test.ts (1)

15-22: Consider verifying the guardedPaymentStatusUpdate mock's note argument.

The mock assertion at lines 130-137 uses expect.objectContaining(...) but doesn't check the note field. The webhook code sets note: \event:${args.eventId}:${args.status}`. Asserting on note` would catch accidental changes to the audit trail string.

Comment thread frontend/lib/services/orders/monobank-webhook.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants