Skip to content

Ezra/309 unapprove distributions - #312

Merged
ND68 merged 4 commits into
mainfrom
ezra/309-unapprove-distributions
Apr 2, 2026
Merged

ND68 merged 4 commits into
mainfrom
ezra/309-unapprove-distributions

Conversation

@ezraexe

@ezraexe ezraexe commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Description

Resolves ticket number: #309

Explain what your code changes:

DistributionTable.tsx - added unapprove function and conditional button rendering
allocationService.ts - signOffId validation in updateAllocationBatch

List the steps you took to test your code:

Tested the unapprove button and approve button within the distributions table, making sure the conditional button is correct, making sure the status changes, and the correct toast notification pops up

Also handling edge case where the shipments are already signed off, if they are signed off, transfers cannot occur. If this is something we should add, I can definitely look into that.

Verified within the prisma database that transferred allocation's are correctly updating(distributionId and partnerId columns)

Also logged into the partner account to confirm that the transferred items appear in their distributions view after approval

image image image

Checklist

  • The ticket is mentioned above
  • The changes fulfill the success criteria of the ticket
  • The changes were self-reviewed
  • A review was requested

@ezraexe
ezraexe requested a review from ND68 March 17, 2026 18:46
@ezraexe ezraexe linked an issue Mar 17, 2026 that may be closed by this pull request
@netlify

netlify Bot commented Mar 17, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for hope-for-haiti ready!

Name Link
🔨 Latest commit 587b6bd
🔍 Latest deploy log https://app.netlify.com/projects/hope-for-haiti/deploys/69b9a920f07a250008b38e28
😎 Deploy Preview https://deploy-preview-312--hope-for-haiti.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.

@greptile-apps

greptile-apps Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the ability to unapprove (revert to pending) a previously approved distribution, complementing the existing approve action. On the frontend a conditional Approve/Unapprove button is rendered based on distribution.pending, and on the backend a signOffId !== null guard is added to updateAllocationBatch to block transferring signed-off allocations.

Key observations:

  • Data integrity gap (logic): updateDistribution in distributionService.ts allows unapproving a distribution even when some of its allocations already have a signOffId set. After unapproving, those allocations are invisible to the partner (partner view filters on pending === false) and cannot be transferred (blocked by the new guard). There is no way to recover them, creating permanently orphaned records. A pre-flight count of signed-off allocations should be added before the pending flip is allowed.
  • Error handling (style): unapproveDistribution (and the pre-existing approveDistribution) don't wrap await promise in a try/finally, so setIsDropdownOpen(false) and fetchTableData() are skipped whenever the API call fails, leaving the dropdown open.
  • Checklist items needing attention:
    • Error responses from the unapprove API call are not handled on the frontend (dropdown stays open).
    • The signed-off-allocation edge case is acknowledged in the PR description but is not enforced at the API layer.

Confidence Score: 2/5

  • Not safe to merge without addressing the signed-off allocation data integrity issue.
  • The happy path (unapproving a distribution with no signed-off allocations) works correctly, but there is a concrete, reproducible scenario where unapproving a distribution that already has signed-off allocations leaves those allocations permanently unreachable — invisible to the partner and blocked from transfer. This is a data integrity issue without a recovery path and should be fixed before merge. The frontend error-handling gap is a secondary concern.
  • Pay close attention to src/services/distributionService.ts (not in the diff but directly impacted) — updateDistribution needs a signed-off allocation guard. Also review src/components/DistributionTable.tsx for the try/finally error handling fix.

Important Files Changed

Filename Overview
src/components/DistributionTable.tsx Adds unapproveDistribution function and conditional Approve/Unapprove button rendering; the new function mirrors the existing approveDistribution pattern but inherits its error-handling gap — if the API call fails, the dropdown is never closed and the table is never refreshed.
src/services/allocationService.ts Adds a signOffId !== null guard in updateAllocationBatch to block transfers of signed-off allocations, but there is no matching check in DistributionService.updateDistribution to prevent unapproving a distribution that already contains signed-off allocations, which can leave those allocations permanently unreachable.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin (UI)
    participant DT as DistributionTable.tsx
    participant API as PATCH /api/distributions/[id]
    participant DS as DistributionService
    participant AS as AllocationService
    participant DB as Database

    Note over Admin,DB: Unapprove flow
    Admin->>DT: Click "Unapprove"
    DT->>API: PATCH {pending: "true"}
    API->>DS: updateDistribution(id, {pending: true})
    Note over DS: ⚠️ No check for signed-off allocations
    DS->>DB: UPDATE distribution SET pending = true
    DB-->>DS: Updated distribution
    DS-->>API: Distribution returned
    API-->>DT: 200 OK
    DT->>DT: Show success toast & reload table

    Note over Admin,DB: Transfer attempt after unapprove
    Admin->>AS: updateAllocationBatch(ids, update)
    AS->>DB: findMany allocations (include distribution)
    DB-->>AS: Allocations with distribution
    AS->>AS: Check distribution.pending (passes ✓)
    AS->>AS: Check signOffId !== null
    alt signOffId is set
        AS-->>Admin: ❌ ArgumentError: Cannot transfer signed-off items
        Note over Admin,DB: Allocations now permanently unreachable
    else signOffId is null
        AS->>DB: updateMany allocations
        DB-->>AS: Updated
        AS-->>Admin: ✅ Transfer complete
    end
Loading

Last reviewed commit: ac36cf3

Comment on lines +177 to +195
async function unapproveDistribution() {
const formData = new FormData();
formData.append("pending", "true");

const promise = apiClient.patch(`/api/distributions/${distribution.id}`, {
body: formData,
});

toast.promise(promise, {
loading: "Unapproving distribution...",
success: "Distribution unapproved!",
error: "Failed to unapprove distribution.",
});

await promise;

setIsDropdownOpen(false);
fetchTableData();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dropdown stays open on API error

If await promise rejects (e.g. the server returns an error), the lines after it — setIsDropdownOpen(false) and fetchTableData() — will never run. The dropdown will stay open even after the error toast is shown. The same pattern exists in approveDistribution. Using a try/finally block ensures cleanup always happens:

Suggested change
async function unapproveDistribution() {
const formData = new FormData();
formData.append("pending", "true");
const promise = apiClient.patch(`/api/distributions/${distribution.id}`, {
body: formData,
});
toast.promise(promise, {
loading: "Unapproving distribution...",
success: "Distribution unapproved!",
error: "Failed to unapprove distribution.",
});
await promise;
setIsDropdownOpen(false);
fetchTableData();
}
async function unapproveDistribution() {
const formData = new FormData();
formData.append("pending", "true");
const promise = apiClient.patch(`/api/distributions/${distribution.id}`, {
body: formData,
});
toast.promise(promise, {
loading: "Unapproving distribution...",
success: "Distribution unapproved!",
error: "Failed to unapprove distribution.",
});
try {
await promise;
fetchTableData();
} finally {
setIsDropdownOpen(false);
}
}

The same fix should be applied to approveDistribution (lines 157–175).

@ezraexe ezraexe Mar 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

commited these changes for both approval and unapproval

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fix looks good! Both approveDistribution and unapproveDistribution now properly use try/finally blocks. The dropdown will close even if the API call fails, which resolves the issue.

The implementation is correct - fetchTableData() stays inside the try block so it only runs on success, which is the right behavior.

Comment on lines +320 to +324
if (allocation.signOffId !== null) {
throw new ArgumentError(
"Cannot transfer items that have been signed off."
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Signed-off allocations in pending distributions become permanently stuck

The new guard correctly prevents transferring signed-off allocations out of a distribution. However, updateDistribution in distributionService.ts has no corresponding check — an admin can successfully unapprove a distribution that already contains signed-off allocations.

After unapproving such a distribution:

  1. Partners can no longer see those allocations (because getPartnerAllocations filters on distribution.pending === false).
  2. The allocations cannot be transferred elsewhere (blocked by this new check).
  3. There is no API path to clear the signOffId, so the allocations are effectively orphaned.

To avoid this stuck state, updateDistribution should validate that no allocations have a non-null signOffId before allowing the transition back to pending:

// In distributionService.ts -> updateDistribution, before db.distribution.update()
if (data.pending === true && currentDistribution.pending === false) {
  const signedOffCount = await db.allocation.count({
    where: { distributionId, signOffId: { not: null } },
  });
  if (signedOffCount > 0) {
    throw new ArgumentError(
      "Cannot unapprove a distribution that has signed-off allocations."
    );
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

commited these changes as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perfect! The fix is implemented correctly in distributionService.ts. The validation check at lines 670-679 prevents unapproving distributions that contain signed-off allocations, which closes the stuck-state gap.

The implementation:

  • ✅ Checks the state transition (approved → pending)
  • ✅ Counts allocations with non-null signOffId
  • ✅ Throws a clear error message if any exist

Combined with the transfer guard in allocationService.ts, signed-off allocations are now properly protected from both operations that would orphan them.

One minor style note: line 672 has inconsistent spacing in { not: null} - should be { not: null } for consistency, but this doesn't affect functionality.

The PR is solid!

@ND68 ND68 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

code looks good, thanks for handling the edge cases, nice work

@ND68
ND68 merged commit 00da022 into main Apr 2, 2026
5 checks passed
@ND68
ND68 deleted the ezra/309-unapprove-distributions branch April 2, 2026 03:14
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.

Unapprove Distributions

2 participants