2026-06-18 00:18:05 -05:00
|
|
|
"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.assignDailyQuestionCallable = exports.assignDailyQuestion = void 0;
|
|
|
|
|
const functions = __importStar(require("firebase-functions"));
|
|
|
|
|
const admin = __importStar(require("firebase-admin"));
|
|
|
|
|
const CST_OFFSET_HOURS = -6;
|
|
|
|
|
/**
|
|
|
|
|
* Scheduled function that assigns one daily question per couple every day.
|
|
|
|
|
*
|
|
|
|
|
* Schedule: 6:00 PM CST (America/Chicago) == 23:00 UTC.
|
|
|
|
|
* Document path: couples/{coupleId}/daily_question/{date}
|
|
|
|
|
* - questionId: string
|
|
|
|
|
* - date: string (YYYY-MM-DD)
|
|
|
|
|
* - assignedAt: Timestamp
|
|
|
|
|
* - expiresAt: Timestamp (next day at 6 PM CST)
|
|
|
|
|
*
|
|
|
|
|
* Admin SDK bypasses Firestore rules; the `daily_question` doc is server-write
|
|
|
|
|
* only (`allow write: if false` in rules).
|
|
|
|
|
*/
|
|
|
|
|
exports.assignDailyQuestion = functions.pubsub
|
|
|
|
|
.schedule('0 23 * * *')
|
|
|
|
|
.timeZone('America/Chicago')
|
|
|
|
|
.onRun(async () => {
|
|
|
|
|
const db = admin.firestore();
|
|
|
|
|
const today = cstDateString();
|
|
|
|
|
const nextDay = nextCstDateString();
|
|
|
|
|
const questionId = await pickRandomQuestionId();
|
|
|
|
|
if (!questionId) {
|
|
|
|
|
console.error('[assignDailyQuestion] no active questions available');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const couplesSnap = await db.collection('couples').get();
|
|
|
|
|
const assignedAt = admin.firestore.FieldValue.serverTimestamp();
|
|
|
|
|
const expiresAt = timestampAt6PmCst(nextDay);
|
|
|
|
|
const writes = couplesSnap.docs.map(async (coupleDoc) => {
|
|
|
|
|
var _a;
|
|
|
|
|
const coupleId = coupleDoc.id;
|
|
|
|
|
const docRef = db
|
|
|
|
|
.collection('couples')
|
|
|
|
|
.doc(coupleId)
|
|
|
|
|
.collection('daily_question')
|
|
|
|
|
.doc(today);
|
|
|
|
|
try {
|
|
|
|
|
await docRef.create({
|
|
|
|
|
questionId,
|
|
|
|
|
date: today,
|
|
|
|
|
assignedAt,
|
|
|
|
|
expiresAt,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (err) {
|
|
|
|
|
if ((err === null || err === void 0 ? void 0 : err.code) === 6 || ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('ALREADY_EXISTS'))) {
|
|
|
|
|
// Already assigned for today — idempotent.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.error(`[assignDailyQuestion] failed for ${coupleId}:`, err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
await Promise.all(writes);
|
|
|
|
|
console.log(`[assignDailyQuestion] assigned ${questionId} to ${couplesSnap.size} couples for ${today}`);
|
|
|
|
|
});
|
|
|
|
|
/**
|
|
|
|
|
* Callable function for immediate daily question assignment.
|
|
|
|
|
*
|
|
|
|
|
* Useful when a couple is newly created and should not wait for the next
|
|
|
|
|
* scheduled run, or for manual admin/testing triggers.
|
|
|
|
|
*
|
|
|
|
|
* Body: { coupleId: string, date?: string }
|
|
|
|
|
*/
|
|
|
|
|
exports.assignDailyQuestionCallable = functions.https.onCall(async (data, context) => {
|
|
|
|
|
var _a, _b, _c, _d;
|
|
|
|
|
const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid;
|
|
|
|
|
if (!callerId) {
|
|
|
|
|
throw new functions.https.HttpsError('unauthenticated', 'Caller must be authenticated.');
|
|
|
|
|
}
|
2026-06-23 10:56:42 -05:00
|
|
|
if (!context.app) {
|
|
|
|
|
throw new functions.https.HttpsError('failed-precondition', 'App Check verification required.');
|
|
|
|
|
}
|
2026-06-18 00:18:05 -05:00
|
|
|
const coupleId = data === null || data === void 0 ? void 0 : data.coupleId;
|
|
|
|
|
if (!coupleId || typeof coupleId !== 'string') {
|
|
|
|
|
throw new functions.https.HttpsError('invalid-argument', 'coupleId is required.');
|
|
|
|
|
}
|
|
|
|
|
const db = admin.firestore();
|
|
|
|
|
const coupleDoc = await db.collection('couples').doc(coupleId).get();
|
|
|
|
|
if (!coupleDoc.exists) {
|
|
|
|
|
throw new functions.https.HttpsError('not-found', 'Couple not found.');
|
|
|
|
|
}
|
|
|
|
|
// Caller must be a member of the couple.
|
|
|
|
|
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 couple member.');
|
|
|
|
|
}
|
|
|
|
|
const date = (data === null || data === void 0 ? void 0 : data.date) && typeof data.date === 'string' ? data.date : cstDateString();
|
|
|
|
|
const questionId = await pickRandomQuestionId();
|
|
|
|
|
if (!questionId) {
|
|
|
|
|
throw new functions.https.HttpsError('internal', 'No active questions available.');
|
|
|
|
|
}
|
|
|
|
|
const nextDay = nextCstDateString(date);
|
|
|
|
|
const docRef = db
|
|
|
|
|
.collection('couples')
|
|
|
|
|
.doc(coupleId)
|
|
|
|
|
.collection('daily_question')
|
|
|
|
|
.doc(date);
|
|
|
|
|
try {
|
|
|
|
|
await docRef.create({
|
|
|
|
|
questionId,
|
|
|
|
|
date,
|
|
|
|
|
assignedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
|
|
|
expiresAt: timestampAt6PmCst(nextDay),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (err) {
|
|
|
|
|
if ((err === null || err === void 0 ? void 0 : err.code) === 6 || ((_d = err === null || err === void 0 ? void 0 : err.message) === null || _d === void 0 ? void 0 : _d.includes('ALREADY_EXISTS'))) {
|
|
|
|
|
throw new functions.https.HttpsError('already-exists', `Daily question already assigned for ${date}.`);
|
|
|
|
|
}
|
|
|
|
|
throw new functions.https.HttpsError('internal', 'Failed to assign daily question.');
|
|
|
|
|
}
|
|
|
|
|
return { success: true, coupleId, date, questionId };
|
|
|
|
|
});
|
|
|
|
|
/**
|
|
|
|
|
* Picks a random active free question from the Firestore `questions` pool.
|
|
|
|
|
*
|
|
|
|
|
* The pool is expected to be a top-level collection with documents:
|
|
|
|
|
* - id: string
|
|
|
|
|
* - active: boolean
|
|
|
|
|
* - isPremium: boolean
|
|
|
|
|
*
|
|
|
|
|
* If no Firestore pool exists yet, falls back to a deterministic placeholder
|
|
|
|
|
* so the app still functions during rollout. In production, keep the local
|
|
|
|
|
* Room database and Firestore pool in sync through the existing seed flow.
|
|
|
|
|
*/
|
|
|
|
|
async function pickRandomQuestionId() {
|
|
|
|
|
const db = admin.firestore();
|
|
|
|
|
const snapshot = await db
|
|
|
|
|
.collection('questions')
|
|
|
|
|
.where('active', '==', true)
|
|
|
|
|
.where('isPremium', '==', false)
|
|
|
|
|
.get();
|
|
|
|
|
if (snapshot.empty) {
|
|
|
|
|
// Rollout fallback: keep the feature working until the questions pool is seeded.
|
|
|
|
|
return 'q_default_daily';
|
|
|
|
|
}
|
|
|
|
|
const docs = snapshot.docs;
|
|
|
|
|
const chosen = docs[Math.floor(Math.random() * docs.length)];
|
|
|
|
|
return chosen.id;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Returns today's date as YYYY-MM-DD in America/Chicago time.
|
|
|
|
|
*/
|
|
|
|
|
function cstDateString() {
|
|
|
|
|
return formatCstDate(new Date());
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Returns tomorrow's date as YYYY-MM-DD relative to America/Chicago time.
|
|
|
|
|
*/
|
|
|
|
|
function nextCstDateString(from) {
|
|
|
|
|
if (from) {
|
|
|
|
|
const d = parseCstDate(from);
|
|
|
|
|
d.setUTCDate(d.getUTCDate() + 1);
|
|
|
|
|
return formatCstDate(d);
|
|
|
|
|
}
|
|
|
|
|
const d = new Date();
|
|
|
|
|
const cst = applyCstOffset(d);
|
|
|
|
|
cst.setUTCDate(cst.getUTCDate() + 1);
|
|
|
|
|
return formatCstDate(cst);
|
|
|
|
|
}
|
|
|
|
|
function applyCstOffset(d) {
|
|
|
|
|
const utcMs = d.getTime();
|
|
|
|
|
const cstMs = utcMs + CST_OFFSET_HOURS * 60 * 60 * 1000;
|
|
|
|
|
return new Date(cstMs);
|
|
|
|
|
}
|
|
|
|
|
function formatCstDate(d) {
|
|
|
|
|
const cst = applyCstOffset(d);
|
|
|
|
|
const y = cst.getUTCFullYear();
|
|
|
|
|
const m = String(cst.getUTCMonth() + 1).padStart(2, '0');
|
|
|
|
|
const day = String(cst.getUTCDate()).padStart(2, '0');
|
|
|
|
|
return `${y}-${m}-${day}`;
|
|
|
|
|
}
|
|
|
|
|
function parseCstDate(dateStr) {
|
|
|
|
|
const [y, m, day] = dateStr.split('-').map(Number);
|
|
|
|
|
// Build a UTC timestamp that represents midnight CST, then reverse the offset.
|
|
|
|
|
const cstMidnightMs = Date.UTC(y, m - 1, day, 0, 0, 0, 0);
|
|
|
|
|
return new Date(cstMidnightMs - CST_OFFSET_HOURS * 60 * 60 * 1000);
|
|
|
|
|
}
|
|
|
|
|
function timestampAt6PmCst(dateStr) {
|
|
|
|
|
const d = parseCstDate(dateStr);
|
|
|
|
|
const utcMs = d.getTime();
|
|
|
|
|
// 6:00 PM CST == 18:00 CST == 00:00 UTC next day.
|
|
|
|
|
// We already have midnight CST in UTC form; add 18 hours.
|
|
|
|
|
const at6pmCstMs = utcMs + 18 * 60 * 60 * 1000;
|
|
|
|
|
return admin.firestore.Timestamp.fromMillis(at6pmCstMs);
|
|
|
|
|
}
|
|
|
|
|
//# sourceMappingURL=assignDailyQuestion.js.map
|