feat(attendance): add notification delivery and NFC security foundation

This commit is contained in:
zouantchaw
2026-03-16 17:06:17 +01:00
parent 5d8240ed51
commit 73287f42bd
21 changed files with 1734 additions and 36 deletions

View File

@@ -0,0 +1,107 @@
CREATE TABLE IF NOT EXISTS device_push_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
staff_id UUID REFERENCES staffs(id) ON DELETE SET NULL,
business_membership_id UUID REFERENCES business_memberships(id) ON DELETE SET NULL,
vendor_membership_id UUID REFERENCES vendor_memberships(id) ON DELETE SET NULL,
provider TEXT NOT NULL DEFAULT 'FCM'
CHECK (provider IN ('FCM', 'APNS', 'WEB_PUSH')),
platform TEXT NOT NULL
CHECK (platform IN ('IOS', 'ANDROID', 'WEB')),
push_token TEXT NOT NULL,
token_hash TEXT NOT NULL,
device_id TEXT,
app_version TEXT,
app_build TEXT,
locale TEXT,
timezone TEXT,
notifications_enabled BOOLEAN NOT NULL DEFAULT TRUE,
invalidated_at TIMESTAMPTZ,
invalidation_reason TEXT,
last_registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_delivery_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_device_push_tokens_membership_scope
CHECK (
business_membership_id IS NOT NULL
OR vendor_membership_id IS NOT NULL
OR staff_id IS NOT NULL
OR user_id IS NOT NULL
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_device_push_tokens_provider_hash
ON device_push_tokens (provider, token_hash);
CREATE INDEX IF NOT EXISTS idx_device_push_tokens_user_active
ON device_push_tokens (user_id, last_seen_at DESC)
WHERE invalidated_at IS NULL AND notifications_enabled = TRUE;
CREATE INDEX IF NOT EXISTS idx_device_push_tokens_staff_active
ON device_push_tokens (staff_id, last_seen_at DESC)
WHERE staff_id IS NOT NULL AND invalidated_at IS NULL AND notifications_enabled = TRUE;
CREATE TABLE IF NOT EXISTS notification_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
notification_outbox_id UUID NOT NULL REFERENCES notification_outbox(id) ON DELETE CASCADE,
device_push_token_id UUID REFERENCES device_push_tokens(id) ON DELETE SET NULL,
provider TEXT NOT NULL DEFAULT 'FCM'
CHECK (provider IN ('FCM', 'APNS', 'WEB_PUSH')),
delivery_status TEXT NOT NULL
CHECK (delivery_status IN ('SIMULATED', 'SENT', 'FAILED', 'INVALID_TOKEN', 'SKIPPED')),
provider_message_id TEXT,
attempt_number INTEGER NOT NULL DEFAULT 1 CHECK (attempt_number >= 1),
error_code TEXT,
error_message TEXT,
response_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_outbox_created
ON notification_deliveries (notification_outbox_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_token_created
ON notification_deliveries (device_push_token_id, created_at DESC)
WHERE device_push_token_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS attendance_security_proofs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
assignment_id UUID NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE,
staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT,
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
event_type TEXT NOT NULL
CHECK (event_type IN ('CLOCK_IN', 'CLOCK_OUT')),
source_type TEXT NOT NULL
CHECK (source_type IN ('NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM')),
device_id TEXT,
nfc_tag_uid TEXT,
proof_nonce TEXT,
proof_timestamp TIMESTAMPTZ,
request_fingerprint TEXT,
attestation_provider TEXT
CHECK (attestation_provider IS NULL OR attestation_provider IN ('PLAY_INTEGRITY', 'APP_ATTEST', 'DEVICE_CHECK')),
attestation_token_hash TEXT,
attestation_status TEXT NOT NULL DEFAULT 'NOT_PROVIDED'
CHECK (attestation_status IN ('NOT_PROVIDED', 'RECORDED_UNVERIFIED', 'VERIFIED', 'REJECTED', 'BYPASSED')),
attestation_reason TEXT,
object_uri TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_attendance_security_proofs_nonce
ON attendance_security_proofs (tenant_id, proof_nonce)
WHERE proof_nonce IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_attendance_security_proofs_assignment_created
ON attendance_security_proofs (assignment_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_attendance_security_proofs_staff_created
ON attendance_security_proofs (staff_id, created_at DESC);