"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.submitOutcomeCallable = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); const DAY_KEYS = ['day_0', 'day_30', 'day_60', 'day_90']; const SCORE_KEYS = ['connection', 'communication', 'intimacy', 'happiness']; const MIN_SCORE = 1; const MAX_SCORE = 10; function isValidDayKey(value) { return typeof value === 'string' && DAY_KEYS.includes(value); } function isValidScoreMap(value) { if (typeof value !== 'object' || value === null || Array.isArray(value)) return false; const map = value; for (const key of SCORE_KEYS) { const num = map[key]; if (typeof num !== 'number' || Number.isNaN(num)) return false; if (num < MIN_SCORE || num > MAX_SCORE) return false; } return true; } exports.submitOutcomeCallable = functions.https.onCall(async (data, context) => { var _a, _b, _c; const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); } const coupleId = data === null || data === void 0 ? void 0 : data.coupleId; if (typeof coupleId !== 'string' || coupleId.length === 0) { throw new functions.https.HttpsError('invalid-argument', 'coupleId is required.'); } const dayKey = data === null || data === void 0 ? void 0 : data.dayKey; if (!isValidDayKey(dayKey)) { throw new functions.https.HttpsError('invalid-argument', `dayKey must be one of: ${DAY_KEYS.join(', ')}.`); } const scores = data === null || data === void 0 ? void 0 : data.scores; if (!isValidScoreMap(scores)) { throw new functions.https.HttpsError('invalid-argument', `scores must contain ${SCORE_KEYS.join(', ')} with values ${MIN_SCORE}-${MAX_SCORE}.`); } const db = admin.firestore(); const coupleRef = db.collection('couples').doc(coupleId); // Caller must be a member of the couple. const coupleDoc = await coupleRef.get(); if (!coupleDoc.exists) { throw new functions.https.HttpsError('not-found', 'Couple not found.'); } const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); if (!userIds.includes(callerId)) { throw new functions.https.HttpsError('permission-denied', 'Caller is not a member of this couple.'); } const now = admin.firestore.Timestamp.now(); const outcomeRef = coupleRef.collection('outcomes').doc(dayKey); const result = await db.runTransaction(async (tx) => { var _a, _b, _c, _d, _e, _f, _g, _h; const existing = await tx.get(outcomeRef); const existingData = existing.exists ? ((_a = existing.data()) !== null && _a !== void 0 ? _a : {}) : {}; // If this is a follow-up (non-baseline), a baseline must exist to compute delta. if (dayKey !== 'day_0') { const baselineRef = coupleRef.collection('outcomes').doc('day_0'); const baselineSnap = await tx.get(baselineRef); if (!baselineSnap.exists) { throw new functions.https.HttpsError('failed-precondition', 'Baseline (day_0) outcome must be submitted before follow-up outcomes.'); } } const answeredBy = ((_b = existingData.answeredBy) !== null && _b !== void 0 ? _b : []); if (!answeredBy.includes(callerId)) { answeredBy.push(callerId); } const payload = { dayKey, answeredBy, updatedAt: now, createdAt: (_c = existingData.createdAt) !== null && _c !== void 0 ? _c : now, }; if (dayKey === 'day_0') { payload.baseline = scores; } else { payload.scores = scores; } // Delta is recalculated every time so repeated submissions stay consistent. if (dayKey !== 'day_0') { const baselineRef = coupleRef.collection('outcomes').doc('day_0'); const baselineSnap = await tx.get(baselineRef); if (baselineSnap.exists) { const baseline = ((_e = (_d = baselineSnap.data()) === null || _d === void 0 ? void 0 : _d.baseline) !== null && _e !== void 0 ? _e : {}); const delta = {}; for (const key of SCORE_KEYS) { delta[key] = ((_f = scores[key]) !== null && _f !== void 0 ? _f : 0) - ((_g = baseline[key]) !== null && _g !== void 0 ? _g : 0); } payload.delta = delta; } } tx.set(outcomeRef, payload, { merge: true }); // Per-user mirror for cross-relationship stats. const userOutcomeRef = db.collection('users').doc(callerId).collection('outcomes').doc(dayKey); tx.set(userOutcomeRef, Object.assign(Object.assign({ dayKey, coupleId }, (dayKey === 'day_0' ? { baseline: scores } : { scores, delta: payload.delta })), { answeredBy: [callerId], createdAt: (_h = existingData.createdAt) !== null && _h !== void 0 ? _h : now, updatedAt: now }), { merge: true }); return { dayKey, answeredBy }; }); console.log(`[submitOutcomeCallable] ${callerId} submitted ${dayKey} for couple ${coupleId}`); return Object.assign({ success: true }, result); }); //# sourceMappingURL=submitOutcomeCallable.js.map