Skip to content

feat: free setting, replies that reach the customer, and an inbox composer - #364

Merged
pratyush618 merged 5 commits into
mainfrom
feat/pricing-and-inbox-compose
Sep 22, 2026
Merged

pratyush618 merged 5 commits into
mainfrom
feat/pricing-and-inbox-compose

Conversation

@kartikeya-27

Copy link
Copy Markdown
Contributor

Four changes, in the order they were asked for.

Setting a made-to-order chapter is free

PRICING.setting goes from 149 to 0. A chapter nobody stocks now costs exactly what the same chapter off the shelf costs, so "not in the inventory" stops being a surcharge. The quote keeps the line rather than dropping it — "we wrote this one for you and did not charge for it" is worth saying, and it renders as free beside the board questions and the NCERT exercise. The screenshot's ₹168 total becomes ₹19.

The number stays in pricing.ts rather than being deleted with the machinery around it: the setting fee is charged once per request and never per copy, and that rule is worth keeping intact for the day it is priced again.

Chapter (₹19) and the subject/board/HOTS sets (₹199/₹499/₹699) are unchanged.

First, one place to change it

The hero trace, the SEO description and two test assertions typed the chapter price out by hand, so changing PRICING left them quoting the old figure. The description is the worst of the three — it is what a search result and a link preview show somebody who has not opened the site yet. All four now read the price list, which is what made the change above a one-line edit.

Replies went to our own alias instead of to the customer

A conversation's correspondent came off the From header and nothing ever looked at Reply-To. The academy sends its work orders from its own sending alias with the customer in Reply-To, so the console offered to reply to ByteVeda, from ByteVeda, about a sheet a student was waiting for.

replyTargetOf is the rule: Reply-To when it is present and parses to a real address that is not the sender again, otherwise From. Header names are matched case-insensitively, only the first address of several is taken, and inbound_messages.from_email still records who actually sent it — the row is the record of what arrived, and only the conversation's correspondent is the reply target.

The thread key stays keyed on the sender, so nothing already filed is re-grouped.

This heals the threads already in the inbox. The headers are stored, and repairBody — which backfills a message fetched after the fact — now re-derives the correspondent alongside them. It only ever narrows towards the truth: a message with no Reply-To rewrites the correspondent to what it already was.

Composing a new message

The console could only answer mail. That stops being enough the moment the job is sending a sheet: the work order names a customer who has never emailed this inbox, and reaching them meant another mail client and no record of it here.

A Compose button in the inbox header opens a dialog with a from-address, a recipient, a subject, a body and the same attachment picker the reply box uses. What it sends opens a conversation keyed the way an inbound reply will be keyed, so their answer lands in it rather than starting a second thread beside it.

  • From is offered from the mailboxes that have actually received mail, which is the same thing as "verified" — every one of them arrived through Resend's inbound routing. A hand-kept list would be a second place to be wrong, and the failure mode is a send refused long after the message was written.
  • The list is scoped to the workspaces the operator may read, and composeMessage re-checks the chosen address against it rather than trusting the form. Somebody with only the academy's mail cannot send as ByteVeda.
  • Attachments get a new compose:<draft id> scope, because there is no conversation to key them to until the send happens. The draft id is minted in the browser and survives a reload with the rest of the draft, so a file uploaded before the address was decided still belongs to the message that goes out.
  • The thread is opened before the send — outbound_messages points at it by foreign key, and a failed attempt belongs in a conversation somebody can open, with its error on it.
  • A failed send keeps the words and the files exactly where they are, so the retry is the same button rather than typing it out and uploading everything again.

Checks

pnpm -r typecheck clean. 229 admin tests and 32 academy tests pass, including six new ones for replyTargetOf covering the academy's exact shape, header capitalisation, multiple addresses and the ignore cases. next build clean for both apps. Biome clean repo-wide.

Not visually verified — both surfaces are behind a login.

Note for whoever merges second

This adds .dialog-head, .dialog-sub, .dialog-close, .dialog-body and .dialog-status to admin.css, and scopes .dialog-card p to .dialog-card > p. #363 adds the same block for its own dialogs. Expect a conflict there; keeping either copy is correct, they are identical.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
byteveda-site Ready Ready Preview Sep 22, 2026 6:25am UTC
byteveda-site-academy Ready Ready Preview Sep 22, 2026 6:25am UTC
byteveda-site-admin Ready Ready Preview Sep 22, 2026 6:25am UTC
1 Skipped Deployment
Project Deployment Actions Updated
byteveda-site-flexiq Skipped Skipped Sep 22, 2026 6:25am UTC

kartikeya-27 and others added 5 commits September 22, 2026 11:51
The hero trace, the SEO description and two assertions typed the
chapter price out by hand, so changing PRICING left them quoting the
old figure — the description worst of all, since it is the price a
search result shows somebody who has not opened the site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A chapter nobody stocks now costs what the same chapter off the shelf
costs. The line stays in the quote at "free" rather than vanishing —
the labour is worth naming, not hiding in a total.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A conversation's correspondent came off the From header, so the
academy's work orders — sent from our sending alias with the customer
in Reply-To — offered to reply to ourselves. Repairing a body now
re-derives it too, which heals the threads already filed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The console could only answer mail, which is not enough when the job is
sending a sheet to somebody who has never written in. A composed
message opens a conversation keyed the way an inbound reply is keyed,
so their answer lands in it rather than beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The browser suite asserts the totals the page prints, and those moved
when the setting fee did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pratyush618
pratyush618 merged commit 0e2b87f into main Sep 22, 2026
16 checks passed
@pratyush618
pratyush618 deleted the feat/pricing-and-inbox-compose branch September 22, 2026 06:26

This branch was successfully deployed

3 active and 1 inactive deployments
Preview – byteveda-site bd8e9336 Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-admin bd8e9336 Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-academy bd8e9336 Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-flexiq bd8e9336 Deployed Sep 22, 2026 by vercel[bot]
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