feat(slack): ask the other person too, and put rating before sharing
ci / lint-test (push) Successful in 43s

Two halves of the same requirement, one of which was quietly missing.

Sharing split the money but never reached her: she is not in #smarthome,
so the card whose caption said "both verdicts welcome" was one she could
not see. Now a share DMs her a card of her own. A DM rather than adding
her to the channel, so her surface stays "orders that concern me"
instead of the whole house's ops feed. She was already in SLACK_USER_MAP,
so her press files under participant 4.

Only on the press that turns sharing ON, and only when someone else did
the sharing. Re-notifying on every later rating press would turn one
shared meal into a stream of DMs, which is how a nudge gets muted.

Her card carries no share button: she is being told it was shared, not
asked to decide, and two people toggling one split from separate copies
of a card is a race with no upside.

The app still holds no Slack bot token — it returns a notify instruction
and n8n sends it, the same shape as the modal open. If SLACK_USER_MAP has
no id for her the DM is skipped silently: the split is correct and
complete either way, and failing the press over an unaddressable nudge
would be the worse trade.

Card reordered to rate -> details -> share. You judge the food, then
decide who pays for it; asking "was this shared?" first inverts the
order a person thinks in. The status caption moved under the share button
it describes rather than sitting orphaned mid-card.
This commit is contained in:
2026-07-28 18:33:25 +10:00
parent 0595d49d5c
commit 9f0f38449b
3 changed files with 145 additions and 32 deletions
+42 -2
View File
@@ -1,7 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { queryRaw, queryRow } from "@/lib/db";
import { verifySlackSignature, participantForSlackUser } from "@/lib/slack-verify";
import { nudgeBlocks, detailsModal } from "@/lib/slack-blocks";
import {
verifySlackSignature,
participantForSlackUser,
slackUserForParticipant,
} from "@/lib/slack-verify";
import { nudgeBlocks, detailsModal, partnerNudgeBlocks } from "@/lib/slack-blocks";
import {
RATINGS,
OWNER_PARTICIPANT_ID,
@@ -105,14 +109,50 @@ export async function POST(req: NextRequest) {
const state = await nudgeState(transactionId);
if (!state) return NextResponse.json({ text: "That order is no longer in the ledger." });
// Ask the other person for their verdict, but only on the press that turned
// sharing ON — and only when it was someone else who shared it with them.
// Re-notifying on every subsequent rating press would make one shared meal
// a stream of DMs, which is how a useful nudge becomes muted.
const notify =
verb === "share" && state.shared && participantId !== SECOND_CONSUMER_ID
? buildPartnerNotify(state, payload.user?.name)
: null;
return NextResponse.json({
replace_original: true,
blocks: nudgeBlocks(state),
// Notification text for clients that cannot render blocks.
text: `${state.merchant}${state.currency} ${state.total.toFixed(2)}`,
...(notify ? { notify } : {}),
});
}
/**
* The DM payload for the other person, or null if we cannot address them.
*
* Returns an instruction rather than sending: the app holds no Slack bot token,
* so n8n — which already has the credential — makes the call. Same shape as the
* modal open.
*
* Null when SLACK_USER_MAP has no Slack id for the second consumer. Silent
* rather than an error: the split is still correct and complete, and failing
* the whole press because a DM could not be addressed would be worse than the
* missing nudge.
*/
function buildPartnerNotify(
state: Awaited<ReturnType<typeof nudgeState>>,
sharerName?: string
) {
if (!state) return null;
const slackUser = slackUserForParticipant(SECOND_CONSUMER_ID);
if (!slackUser) return null;
return {
user: slackUser,
text: `${state.merchant} — shared with you 50/50`,
blocks: partnerNudgeBlocks(state, sharerName || "It was"),
};
}
/** The modal view, pre-filled with whatever this person already said. */
async function buildDetailsModal(transactionId: number, participantId: number) {
const row = await queryRow<{