Skip to content

Release: promote develop to main (catch up 101 commits, last release 2026-07-02)#755

Open
nadavosa wants to merge 114 commits into
mainfrom
develop
Open

Release: promote develop to main (catch up 101 commits, last release 2026-07-02)#755
nadavosa wants to merge 114 commits into
mainfrom
develop

Conversation

@nadavosa

Copy link
Copy Markdown
Collaborator

What

Production (`main`) hasn't been promoted from `develop` since PR #719 (2026-07-02) — `develop` is currently 101 commits ahead. This release catches production up, including (among other work):

Risk

Recommend reviewing/smoke-testing before merging rather than treating this as routine, given it's a production deploy.

Related

nadavosa and others added 30 commits May 18, 2026 14:34
…, passwd, fetchJsonFromUrl)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The mocking approach didn't work under this repo's swc transform: vi.mock
only intercepts inlined local modules, not externalized node builtins/deps
(bcrypt, fs/promises), so those mocks were never applied and the real
implementations ran. Also removed tests for a non-existent getStartEndDates
function (only getStartEnd exists).

- passwd: test real bcrypt hash/verify behaviour + error wrapping
- fetch-json: read a real temp file for the file-path branches
- get-start-end-dates: drop the getStartEndDates block

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compile-time type tests declare consts solely to trip their
@ts-expect-error directives, so they are never read. Prefix them with `_`
(the escape hatch the no-unused-vars rule is configured to allow) and apply
Prettier formatting.

Side effect: reference the shared .claude/shared-rules.md from CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore(test): fix no-unused-vars lint errors in types.test.ts
…#665)

* ✨ link opportunity contact person to agent_person on legacy form submit

When a legacy form submission creates an opportunity, the contact person
is now also inserted into agent_person (VOLUNTEER_COORDINATOR) for the
matched/created agent. This builds up the full list of known persons per
agent over time without touching the existing representative.

The submitter was already linked via getOrCreateSubmitterPerson; this
covers the contact person which was previously only linked to the
opportunity.

Resolves #664

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#665): use getOrCreateSubmitterPerson for contact person — dedup by email + guarded agent_person insert

Replace the plain parseContactPerson + unconditional AgentPerson save with
getOrCreateSubmitterPerson, which email-deduplicates the Person record and
only inserts agent_person when the link does not already exist.

Also removes the duplicate Person + AgentPerson creation from
findOrCreateAgent's new-agent transaction: the handler now owns person
resolution for both contactPersonId and submittedByPersonId. A bare
parseContactPerson fallback is kept for the no-email case (no dedup
possible, no agent_person link).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Arturas Mickiewicz <57557793+arturasmckwcz@users.noreply.github.com>
Co-authored-by: arturasmckwcz <arturasmckwcz@gmail.com>
…d and POST /volunteer/ (#702)

Exports langQuerySchema from querystring.ts and wires it into the three
volunteer endpoints that accept a language param, so Swagger shows the
parameter correctly.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(notify): extract shared email-template helper

Introduces `email-template.ts` with `fillTemplate`, `createManifestLoader`,
`resolveLocale`, and `resolveContent` so future email types (suggestions,
reminders, matching notifications) reuse the same CDN-manifest + placeholder
infrastructure without duplication.

- `fillTemplate(content, vars)` replaces all `{{ key }}` placeholders via
  regex; unresolved keys are preserved in output and logged as warnings
- `createManifestLoader(url)` is a factory with per-instance TTL cache and
  `resetCache()` for test isolation, replacing the module-level cache pattern
- `email-verification.ts` reduced from 141 → 70 lines; all infrastructure
  imported from the new module, public API unchanged
- 20 new tests in `email-template.test.ts`; all 26 tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: widen TemplateVars to string|number, fix fake timer cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#679): add ActivityLog entity and CRUD endpoints

- ActivityLog entity (date + decimal hours) with FK → OpportunityVolunteer (CASCADE)
- GET /opportunity-volunteer/:id/activity-log — returns entries + totalHours + count
- POST /opportunity-volunteer/:id/activity-log — create entry
- PATCH /activity-log/:id — update entry
- DELETE /activity-log/:id — delete entry
- All routes restricted to COORDINATOR | AGENT | ADMIN roles
- SDK upgraded to 0.0.111 (ApiActivityLog* types)
- Migration: add-activity-log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#679): address review findings on ActivityLog endpoints

- Add transformer to decimal `hours` column so pg driver string is
  coerced to number at entity level instead of leaking as "2.50"
- Extract dtoActivityLogEntry / dtoActivityLogGet; route handlers now
  go through DTO instead of returning raw entities
- Remove redundant id <= 0 guards (idParamSchema already enforces minimum: 1)
- Restrict PATCH/DELETE to COORDINATOR role via per-route authenticate()
  option; AGENT retains GET/POST access only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add nullable `read_at TIMESTAMP` column to `comment_person` (migration)
- Replace `ApiComment.taggedPersonIds: number[]` with
  `taggedPersons: { id, readAt }[]`; update commentSerializer and tests
- Add `ApiCommentTaggedPerson` schema to sdk-types.json
- PATCH /comment/:id/read — sets read_at = now() for the calling user's
  comment_person row; returns updated comment; 404 if user is not tagged
- SDK upgraded to 0.0.112

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(fe#570): expose agentId on opportunity list response

Add agentId to dtoOpportunityGetList and dtoOpportunityGet so the FE
can compare each row's agentId against the authenticated user's agent
and apply column masking for other orgs' opportunities. SDK → 0.0.113.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: agentId nullable in schema; SDK → 0.0.114

agentId type corrected to ["integer", "null"] to match the entity's
nullable column. SDK bumped to 0.0.114 which carries the same fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Build and push ghcr.io/need4deed-org/be:develop on every merge to develop,
then rollout restart the be deployment on the AITS VPS.

Requires AITS_HOST and AITS_SSH_KEY repo secrets.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: add AITS deploy workflow

Build and push ghcr.io/need4deed-org/be:develop on every merge to develop,
then rollout restart the be deployment on the AITS VPS.

Requires AITS_HOST and AITS_SSH_KEY repo secrets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: build bootstrap image; make bootstrap idempotent

- Add build-bootstrap.yaml workflow: builds ghcr.io/need4deed-org/bootstrap:latest
  on changes to data-bootstrap/ or manual dispatch
- Make bootstrap.sh idempotent: skip if language table already exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: add AITS deploy workflow

Build and push ghcr.io/need4deed-org/be:develop on every merge to develop,
then rollout restart the be deployment on the AITS VPS.

Requires AITS_HOST and AITS_SSH_KEY repo secrets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: gitignore data-bootstrap/*.sql — may contain PII

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch bootstrap from dev.dump.sql (raw PII) to scrambled-dump.sql
  (agent.info stripped, person/volunteer data anonymized)
- Add idempotency check: skip if language table already exists
- Narrow .gitignore to dev.dump.sql only so scrambled-dump.sql can be tracked

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#712)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Public POST /user/ blocks admin/coordinator roles by design.
New POST /user/admin requires an active admin session and creates
the account as immediately active (no email verification needed).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ✨ feat: add POST /user/admin for admin-privileged user creation

Public POST /user/ blocks admin/coordinator roles by design.
New POST /user/admin requires an active admin session and creates
the account as immediately active (no email verification needed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add workflow_dispatch to deploy-aits workflow

Allows manual re-deploys without needing a code push.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bump need4deed-sdk 0.0.114 → 0.0.115 (adds agentId to ApiUserGet)
- serializeUserToMeDTO now returns ApiUserGet directly instead of ApiUserGet & { agentId?: number }
- Fix additionalProperties misplacement in sdk-types.json ApiUserMe schema (was inside properties, now a sibling)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only AGENT-role users can have agent memberships; firing the agentPersonRepository
queries for every user with a personId (volunteers, coordinators, admins) was wasted
DB work on the hot session-check endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…MeDTO

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore: upgrade need4deed-sdk to 0.0.115, fix agentId handling in GET /user/me
arturasmckwcz and others added 30 commits July 8, 2026 11:58
Drop numActiveVolunteers from DTO and test (removed from SDK contract).
Update test to use AgentTrustType.HIGH instead of bare number.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: upgrade need4deed-sdk to 0.0.120

Drop numActiveVolunteers from DTO and test (removed from SDK contract).
Update test to use AgentTrustType.HIGH instead of bare number.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: switch to Infomaniak SMTP; redirect dry-run to test@; mute cron flag

Replace Brevo with nodemailer SMTP (Infomaniak). DryRunEmailTransport now
redirects to test@need4deed.org with [TO: <original>] subject prefix and
actually delivers via the real transport. Add NOTIFY_CRON_MUTED env var
to suppress cron email scans without disabling the scheduler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: align SMTP_PASS env var name with infra

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…#741)

SDK 0.0.122 adds the five CommunicationType values that were in the
PostgreSQL enum since migration 1782903516653 but missing from the
TypeScript enum (ACCOMPANYING_MATCHED, MATCHED, OPPORTUNITY_CONFIRMATION,
ACCOMPANYING_NOT_FOUND, OPPORTUNITY_UPDATED) and adds opportunityId to
ApiCommunicationGet — both were blocking tsc compilation.

dtoAgentOpportunity now returns the full ApiAgentOpportunity shape
(volunteerType, district, languages, activities, location, availability).
The agent-opportunity route loads the deal relations needed to populate them.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Coordinators need to rename opportunities from the dashboard. The title
field was present on the Opportunity entity and in GET responses but was
not accepted by the PATCH endpoint.

- Upgrade need4deed-sdk 0.0.122 → 0.0.125 (0.0.125 adds title to
  ApiOpportunityPatch)
- Add "title" to ApiVolunteerOpportunityPatch JSON schema so Fastify/AJV
  accepts it (schema had additionalProperties: false)
- Map body.title into the opportunity object in parseOpportunity so it
  reaches patchEntity(Opportunity, ...)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fastify's AJV coerces null → [null] for { type: "array" } fields.
When creating an event-type opportunity the frontend sends timeslots: null;
that null was coerced to [null], the parser looped over it, and tried to
destructure null as [day, daytime] → TypeError: opportunityTime is not iterable.

Change timeslots to { type: ["array", "null"] } so null passes through
unchanged; the existing parser guard (formData.timeslots || []) then
short-circuits to an empty array and the loop is never entered.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace four copy-pasted Set-intersection validation blocks in the POST
and PATCH /post handlers with a single pure helper that throws a
consistent BadRequestError for missing relation ids.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Infomaniak does not allow multiple senders on one SMTP account, so two
separate transports are needed. The existing SMTP transport (SMTP_HOST /
SMTP_USER / SMTP_PASS) is now exclusively for email-verification and
password-reset. A new transport (SMTP_NOTIFY_HOST / SMTP_NOTIFY_USER /
SMTP_NOTIFY_PASS / EMAIL_FROM_NOTIFY) handles all cron-scan and
status-change notification emails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a cc field to EmailMessage and wire it through SmtpEmailTransport
(passes to nodemailer) and DryRunEmailTransport (strips CC, appends
CC addresses to the [TO: ...] subject prefix for visibility).

Add emailFromAccompanying constant (accompanying@need4deed.org).

CC assignments per trigger:
- emailSuggestion        → cc: volunteer@need4deed.org
- emailAccompanyMatch    → cc: contact@need4deed.org
- emailIntroduction      → cc: [contact@, volunteer@] (was incorrectly
                           stuffed into to[])
- emailNewAccompanying   → cc: [contact@, accompanying@]
- emailStale             → cc: volunteer@need4deed.org
- emailPostMatchCheckup  → cc: volunteer@need4deed.org
- emailAccompanyNotFound → cc: accompanying@need4deed.org
- emailRegularUpdate     → cc: contact@need4deed.org

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EMAIL_FROM_VOLUNTEER, EMAIL_FROM_CONTACT, EMAIL_FROM_ACCOMPANYING fall
back to the existing @need4deed.org defaults when unset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All notification emails now send from coordinators@need4deed.org (the
dedicated notify SMTP account). The cc addresses remain the role-specific
ones (volunteer@, contact@, accompanying@) so replies still reach the
right inbox. EMAIL_FROM_NOTIFY is configurable via env var.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(notify): split SMTP into verify and notify transports
…cations

feat(notify): add CC addresses to outbound notification emails
…treet alone (#750)

The /agent/register/search guard required postcode, but the frontend only
has the street at the time of typing. Make plz optional in getAgentByAddress
(still used as a narrowing filter when provided) and drop !postcode from the
route guard so the lookup fires as soon as street >= 3 chars.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…browser cookie (#751)

n4d_pending_role cookie was fragile — absent when the link is opened in a
different browser than where registration happened. Append ?role=agent to the
verification URL for AGENT users so the redirect target is always in the URL.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* replace bootstrap SQL dump with clean seed infrastructure

- Remove scrambled-dump.sql and bootstrap.sh from data-bootstrap/; the
  pg_dump approach was derived from production data and posed a PII risk
  even when scrambled.

- Add dev/files/fixtures/ with three fake-data JSON files (nid-agents.json,
  nid-opportunities.json, nid-volunteers.json): 8 fake RACs, 12 opportunities,
  15 volunteers, all cross-referenced by NID, zero real personal data.

- Wire populate seeders to the fixtures directory (constants.ts paths updated
  from dev/files/notion/ to dev/files/fixtures/).

- Add src/data/seeds/populate/agent-user.seed.ts: after agents are seeded,
  creates UserRole.USER accounts for the 5 RAC contact persons that carry
  an @example.com email in the fixture.

- Expand user.seed.ts from 1+1 to 1 admin + 3 coordinators (sarah/michael/
  julia.doe) + 1 dummy USER (anna.doe, kept as seed-utils fallback person).

- Add src/data/seeds/run.ts entry point (initDatabase → seed) and a
  "seed" npm script so the bootstrap container can call yarn seed.

- Rewrite data-bootstrap/Dockerfile: drop postgres:17 base, use node:22-alpine,
  install be deps, run yarn seed.

- Update docker-compose.yaml: bootstrap now builds from the be/ root with
  dockerfile: data-bootstrap/Dockerfile; adds NODE_ENV=development and
  RUN_MIGRATIONS=true so migrations run before seeding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* move populate fixtures from dev/ to src/data/seeds/fixtures/

dev/ is gitignored; fixtures are committed source files and belong
alongside the seeder code. Update constants.ts paths accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile copies package.json, yarn.lock, and full source — needs the
be/ root as context, not ./data-bootstrap alone.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The 73-migration chain was written against a pre-existing schema and
cannot replay from scratch — migration #2 fails with 'relation language
does not exist'. This genesis migration (timestamp 1763036250000) runs
first on any DB and:
- Creates all enums, sequences, tables and constraints in their current
  final form using IF NOT EXISTS / DO EXCEPTION blocks (idempotent)
- Marks all 73 existing migrations as already-run in be_migrations so
  TypeORM skips them on both fresh and existing DBs

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
TypeORM builds its full migration queue once by reading be_migrations, then
runs every pending migration in order. Genesis creates the schema and inserts
73 records, but TypeORM already queued all 73 subsequent migrations — they then
fail with "type already exists" conflicts.

bootstrapFreshDb() now runs genesis.up() directly and inserts genesis's own
record into be_migrations BEFORE dataSource.runMigrations() is called. On a
fresh DB TypeORM therefore sees 74/74 migrations already done and runs zero.
On an existing DB (cnt > 0) the function exits immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pre-populate-migrations

fix: pre-populate be_migrations before TypeORM reads it on fresh DB
…uildtime

fix(bootstrap): compile TypeScript at build time to reduce runtime memory
…ixtures

fix(bootstrap): copy public assets and seed fixtures into production stage
…te passes (#758)

* add: date util

* add: job for onetimers expiration

* add: new scheduler plugin for daily jobs and expires opps job

* fix: change to berlin day before

* fix: the same lock key and refactor duple

* fix: refactor plugin names

* fix: update comment

* fix: use german calendar and update util with countdown

* fix: remove stale code

* fix: rename plugins

* fix: add catching errs

* test: add unit tests for scanExpiredOnetimers

Covers the no-op empty-result case, correct INACTIVE/PAST status
transitions, and the per-item try/catch isolation added for opportunity
and opportunity-volunteer saves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…/creation

Multiple notify events hard-dereferenced opportunity.contactPerson, which is
now frequently null on opportunities that only have submittedByPerson set
(post-#589 data). The route side-effects swallow send failures into
logger.error after already responding, so nothing surfaced to callers:

- "suggest" email never sent on OV creation (POST created straight into
  PENDING; the send side-effect only lived in the PATCH transition handler)
- "matched"/accompanying-matched emails threw on contactPerson!.name/.phone
- new-opportunity confirmation emails (regular + accompanying) always threw,
  since the modern creation route never sets contactPersonId at all

Adds getOpportunityRepresentativePerson() (submittedByPerson -> contactPerson
-> agent representative) and wires it into all four notify events. Also adds
a pre-check + 409 for duplicate (opportunityId, volunteerId) suggestions
(previously an unhandled 500 from the DB unique constraint), and removes a
redundant id<=0 param check already covered by the JSON schema.

Fixes #764
…ostcode

POST /opportunity derives the Deal's postcode solely from the owning agent's
address (this form has no rac_plz fallback, unlike the legacy route). deal.
postcode_id is NOT NULL, so an agent without an address/postcode previously
reached the DB as an unhandled 500 (QueryFailedError) instead of a clean
validation error.
- getOpportunityRepresentativePerson() stopped at the first non-null
  candidate even if it had no email (e.g. a submitter whose email lives on
  their linked User row, not their Person row, as with internal
  coordinator/admin accounts). Now prefers whichever candidate actually has
  an email, falling through the chain instead of giving up early.
- sendEmailNewRegular sent with `from: emailFromContact` — the only notify
  event not using `from: emailFromNotify` (the address actually authorized
  on the SMTP account all these emails share). Every send was rejected by
  Infomaniak with "550 5.7.1 Sender mismatch". Fixed to match every sibling
  event: `from: emailFromNotify`, `cc: emailFromContact`.

Verified end-to-end against the local docker compose stack: created three
opportunities via POST /opportunity for agent_id=1, confirmed the
opportunity-confirmation communication log entry and a successful dry-run
send to the agent's representative.
fix: notification emails silently failing across opportunity matching/creation
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.

3 participants