feat: T4b/T4c wave 9 - phenology reports, autopilot A4-A10, honey ledger, Telegram delivery
- Phenology reports API with corrections and revokes; recompute per apiary via PhenologyRecomputeRequested, allowBackward for corrections; season changes are announced in the morning digest (changes since the previous digest, each once) - Season autopilot A4-A10 with per-apiary sync (Apiary before Colony locks) and WinterCheckRecorded - Honey batch ledger at runtime: initial amount, BOTTLING bookings, manual movements, one gram rounding, untracked bottlings never book - Telegram delivery: planning, Berlin delivery times incl. DST, quiet hours, permission re-check and stale skip at send time, rate limits, cleanup of old deliveries - ARCHITECTURE.md: assumptions 47-50 and updated sections 11.x Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1971b2d9c4
commit
16cc557462
74 files changed
+9634
-340
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
Prisma,
|
||||
} from '../../generated/prisma';
|
||||
import { PermissionResolverService } from '../auth/permissions/permission-resolver.service';
|
||||
import { NotificationDeliveryPlanner } from './delivery-planner';
|
||||
import { assertAppLink, notificationDedupeKey } from './notification-link';
|
||||
|
||||
export interface CreateNotificationInput {
|
||||
@@ -87,13 +88,15 @@ function uniqueSorted(keys: readonly PermissionKey[]): PermissionKey[] {
|
||||
* needs (the rule the PermissionsGuard applies). The stored
|
||||
* requiredPermissions are the recipient's and the content's permissions
|
||||
* together, so a later consumer (e.g. the Telegram sender) can check them
|
||||
* again.
|
||||
* again. Right after the insert, in the same transaction, the delivery planner
|
||||
* queues the Telegram delivery for users who asked for it.
|
||||
*/
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissionResolver: PermissionResolverService,
|
||||
private readonly deliveryPlanner: NotificationDeliveryPlanner,
|
||||
) {}
|
||||
|
||||
/** One recipient. null: repeated dedupe key, or the recipient lacks a right. */
|
||||
@@ -274,19 +277,30 @@ export class NotificationsService {
|
||||
...recipientKeys,
|
||||
...(content.requiredPermissions ?? []),
|
||||
]);
|
||||
return db.notification.createManyAndReturn({
|
||||
data: targets.map(({ userId, dedupeKey }) => ({
|
||||
userId,
|
||||
priority: content.priority,
|
||||
category: content.category,
|
||||
message: content.message,
|
||||
relatedType: content.relatedType ?? null,
|
||||
relatedId: content.relatedId ?? null,
|
||||
link: content.link ?? null,
|
||||
requiredPermissions,
|
||||
dedupeKey,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
const insertAndPlan = async (client: DbClient): Promise<Notification[]> => {
|
||||
const rows = await client.notification.createManyAndReturn({
|
||||
data: targets.map(({ userId, dedupeKey }) => ({
|
||||
userId,
|
||||
priority: content.priority,
|
||||
category: content.category,
|
||||
message: content.message,
|
||||
relatedType: content.relatedType ?? null,
|
||||
relatedId: content.relatedId ?? null,
|
||||
link: content.link ?? null,
|
||||
requiredPermissions,
|
||||
dedupeKey,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
// Only the rows that were inserted: a repeated dedupe key plans nothing.
|
||||
await this.deliveryPlanner.planIn(client, rows);
|
||||
return rows;
|
||||
};
|
||||
// Rows and deliveries belong together: without the caller's transaction
|
||||
// they get their own. A caller WITH a transaction gets no nested one: the
|
||||
// insert and the planning run on its client and commit or roll back with it.
|
||||
return '$connect' in db
|
||||
? db.$transaction((tx) => insertAndPlan(tx))
|
||||
: insertAndPlan(db);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user