2026-07-06 11:26:50 -05:00
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req , Res , Next } from '../types/http' ;
2026-05-28 22:54:07 -05:00
const express = require ( 'express' ) ;
const router = express . Router ( ) ;
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:34:08 -05:00
const { getDb , ensureUserDefaultCategories } = require ( '../db/database.cts' ) ;
const { standardizeError } = require ( '../middleware/errorFormatter.cts' ) ;
2026-07-05 13:44:04 -05:00
const { fromCents } = require ( '../utils/money.mts' ) ;
2026-05-28 22:54:07 -05:00
const {
createSubscriptionFromRecommendation ,
2026-05-29 02:51:30 -05:00
declineRecommendation ,
2026-05-28 22:54:07 -05:00
decorateSubscription ,
getSubscriptionRecommendations ,
getSubscriptionSummary ,
getSubscriptions ,
2026-06-06 20:02:13 -05:00
monthlyEquivalent ,
2026-06-06 21:15:08 -05:00
recordSubscriptionFeedback ,
2026-05-30 17:27:15 -05:00
searchSubscriptionTransactions ,
2026-07-06 11:22:44 -05:00
} = require ( '../services/subscriptionService.cts' ) ;
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
const { addMerchantRule , applyMerchantRules } = require ( '../services/billMerchantRuleService.cts' ) ;
2026-05-28 22:54:07 -05:00
2026-07-06 11:26:50 -05:00
router . get ( '/' , ( req : Req , res : Res ) = > {
2026-05-28 22:54:07 -05:00
const db = getDb ( ) ;
ensureUserDefaultCategories ( req . user . id ) ;
const subscriptions = getSubscriptions ( db , req . user . id ) ;
res . json ( {
summary : getSubscriptionSummary ( subscriptions ) ,
subscriptions ,
} ) ;
} ) ;
2026-07-06 11:26:50 -05:00
router . get ( '/recommendations' , ( req : Req , res : Res ) = > {
2026-05-28 22:54:07 -05:00
const db = getDb ( ) ;
res . json ( {
recommendations : getSubscriptionRecommendations ( db , req . user . id ) ,
} ) ;
} ) ;
2026-05-29 02:51:30 -05:00
2026-07-06 11:26:50 -05:00
router . get ( '/transaction-matches' , ( req : Req , res : Res ) = > {
2026-05-30 17:27:15 -05:00
try {
res . json ( {
transactions : searchSubscriptionTransactions ( getDb ( ) , req . user . id , req . query ) ,
} ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to search subscription transactions' , 'SUBSCRIPTION_SEARCH_ERROR' ) ) ;
}
} ) ;
2026-07-06 11:26:50 -05:00
router . post ( '/recommendations/decline' , ( req : Req , res : Res ) = > {
2026-05-29 02:51:30 -05:00
const { decline_key } = req . body || { } ;
if ( ! decline_key || typeof decline_key !== 'string' || decline_key . length > 200 ) {
return res . status ( 400 ) . json ( standardizeError ( 'decline_key is required' , 'VALIDATION_ERROR' , 'decline_key' ) ) ;
}
try {
2026-06-06 21:15:08 -05:00
const db = getDb ( ) ;
declineRecommendation ( db , req . user . id , decline_key ) ;
recordSubscriptionFeedback ( db , req . user . id , {
action : 'decline' ,
catalog_id : req.body?.catalog_id ,
merchant : req.body?.merchant ,
confidence : req.body?.confidence ,
metadata : { decline_key } ,
} ) ;
2026-05-29 02:51:30 -05:00
res . json ( { ok : true } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to decline recommendation' , 'DECLINE_ERROR' ) ) ;
}
} ) ;
2026-05-28 22:54:07 -05:00
2026-05-29 03:38:48 -05:00
// POST /api/subscriptions/recommendations/match-bill
// Link an existing bill to all transactions in a recommendation (no new bill created).
2026-07-06 11:26:50 -05:00
router . post ( '/recommendations/match-bill' , ( req : Req , res : Res ) = > {
2026-05-29 03:38:48 -05:00
const billId = parseInt ( req . body ? . bill_id , 10 ) ;
const rawIds = Array . isArray ( req . body ? . transaction_ids ) ? req . body . transaction_ids : [ ] ;
const txIds = rawIds . map ( id = > parseInt ( id , 10 ) ) . filter ( n = > Number . isInteger ( n ) && n > 0 ) . slice ( 0 , 50 ) ;
if ( ! Number . isInteger ( billId ) || billId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'bill_id is required' , 'VALIDATION_ERROR' , 'bill_id' ) ) ;
}
if ( txIds . length === 0 ) {
return res . status ( 400 ) . json ( standardizeError ( 'transaction_ids must be a non-empty array' , 'VALIDATION_ERROR' , 'transaction_ids' ) ) ;
}
const db = getDb ( ) ;
2026-06-06 21:15:08 -05:00
const bill = db . prepare ( 'SELECT id, name, catalog_id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( billId , req . user . id ) ;
2026-05-29 03:38:48 -05:00
if ( ! bill ) return res . status ( 404 ) . json ( standardizeError ( 'Bill not found' , 'NOT_FOUND' , 'bill_id' ) ) ;
2026-06-06 21:15:08 -05:00
const catalogId = parseInt ( req . body ? . catalog_id , 10 ) ;
const catalogEntry = Number . isInteger ( catalogId ) && catalogId > 0
? db . prepare ( 'SELECT id FROM subscription_catalog WHERE id = ?' ) . get ( catalogId )
: null ;
2026-05-29 03:38:48 -05:00
2026-05-29 04:19:20 -05:00
const placeholders = txIds . map ( ( ) = > '?' ) . join ( ',' ) ;
const txRows = db . prepare ( `
SELECT id , amount , posted_date , transacted_at
FROM transactions
WHERE user_id = ? AND id IN ( $ { placeholders } ) AND ignored = 0 AND match_status != 'matched'
` ).all(req.user.id, ...txIds);
const updateTx = db . prepare ( `
2026-05-29 03:38:48 -05:00
UPDATE transactions
SET matched_bill_id = ? , match_status = 'matched' , updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ? AND ignored = 0 AND match_status != 'matched'
` );
2026-05-29 04:19:20 -05:00
const insertPayment = db . prepare ( `
INSERT OR IGNORE INTO payments ( bill_id , amount , paid_date , payment_source , transaction_id )
VALUES ( ? , ? , ? , 'auto_match' , ? )
` );
2026-05-29 03:38:48 -05:00
let matchedCount = 0 ;
db . transaction ( ( ) = > {
2026-05-29 04:19:20 -05:00
for ( const tx of txRows ) {
const paidDate = tx . posted_date || ( tx . transacted_at ? String ( tx . transacted_at ) . slice ( 0 , 10 ) : null ) ;
2026-06-11 20:12:31 -05:00
const amount = Math . round ( Math . abs ( tx . amount ) ) ; // tx.amount and payments.amount are both cents
2026-05-29 04:19:20 -05:00
matchedCount += updateTx . run ( billId , tx . id , req . user . id ) . changes ;
if ( paidDate ) insertPayment . run ( billId , amount , paidDate , tx . id ) ;
2026-05-29 03:38:48 -05:00
}
} ) ( ) ;
// Store merchant rule for ongoing auto-matching on future syncs
const merchant = typeof req . body ? . merchant === 'string' ? req . body . merchant . trim ( ) : '' ;
if ( merchant ) addMerchantRule ( db , req . user . id , billId , merchant ) ;
2026-06-06 21:15:08 -05:00
if ( catalogEntry && ! bill . catalog_id ) {
try {
db . prepare ( 'UPDATE bills SET catalog_id = ?, updated_at = datetime(\'now\') WHERE id = ? AND user_id = ?' )
. run ( catalogEntry . id , billId , req . user . id ) ;
} catch { /* pre-v0.96 */ }
}
recordSubscriptionFeedback ( db , req . user . id , {
action : 'link_existing_bill' ,
catalog_id : catalogEntry?.id ,
bill_id : billId ,
merchant ,
confidence : req.body?.confidence ,
metadata : { transaction_ids : txIds } ,
} ) ;
2026-05-29 03:38:48 -05:00
// Apply rules immediately to catch any unmatched transactions beyond the explicit list
const { matched : autoMatched } = applyMerchantRules ( db , req . user . id ) ;
res . json ( { ok : true , matched_count : matchedCount + autoMatched , bill_name : bill.name } ) ;
} ) ;
2026-07-06 11:26:50 -05:00
router . post ( '/recommendations/create' , ( req : Req , res : Res ) = > {
2026-05-28 22:54:07 -05:00
const db = getDb ( ) ;
ensureUserDefaultCategories ( req . user . id ) ;
if ( req . body ? . category_id ) {
const categoryId = parseInt ( req . body . category_id , 10 ) ;
const category = Number . isInteger ( categoryId )
? db . prepare ( 'SELECT id FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( categoryId , req . user . id )
: null ;
if ( ! category ) {
return res . status ( 400 ) . json ( standardizeError ( 'category_id is invalid for this user' , 'VALIDATION_ERROR' , 'category_id' ) ) ;
}
}
try {
const created = createSubscriptionFromRecommendation ( db , req . user . id , req . body || { } ) ;
2026-05-29 03:38:48 -05:00
// Store merchant rule so future SimpleFIN transactions auto-match this bill
if ( req . body ? . merchant ) addMerchantRule ( db , req . user . id , created . id , req . body . merchant ) ;
2026-06-06 21:15:08 -05:00
recordSubscriptionFeedback ( db , req . user . id , {
action : 'accept_track_new' ,
catalog_id : req.body?.catalog_match?.id ,
bill_id : created.id ,
merchant : req.body?.merchant ,
confidence : req.body?.confidence ,
metadata : { transaction_ids : req.body?.transaction_ids || [ ] } ,
} ) ;
2026-05-28 22:54:07 -05:00
res . status ( 201 ) . json ( created ) ;
} catch ( err ) {
res . status ( err . status || 400 ) . json ( standardizeError ( err . message || 'Could not create subscription' , err . status ? 'VALIDATION_ERROR' : 'SUBSCRIPTION_CREATE_ERROR' , err . field || null ) ) ;
}
} ) ;
2026-06-06 20:02:13 -05:00
// ── Catalog browser ───────────────────────────────────────────────────────────
2026-07-06 11:26:50 -05:00
router . get ( '/catalog' , ( req : Req , res : Res ) = > {
2026-06-06 20:02:13 -05:00
const db = getDb ( ) ;
try {
const catalogEntries = db . prepare ( `
SELECT id , rank , name , category , subcategory , subscription_type ,
website , starting_monthly_usd , starting_annual_usd
FROM subscription_catalog
ORDER BY rank ASC
` ).all();
if ( ! catalogEntries . length ) return res . json ( { catalog : [ ] } ) ;
// User's subscription bills that are linked to a catalog entry
const matchedBills = db . prepare ( `
SELECT b . id , b . name , b . expected_amount , b . active , b . catalog_id ,
b . cycle_type , b . billing_cycle
FROM bills b
WHERE b . user_id = ?
AND b . is_subscription = 1
AND b . deleted_at IS NULL
AND b . catalog_id IS NOT NULL
` ).all(req.user.id);
const billByCatalogId = new Map ( matchedBills . map ( b = > [ b . catalog_id , b ] ) ) ;
// User's custom descriptors
let userDescriptors = [ ] ;
try {
userDescriptors = db . prepare (
'SELECT id, catalog_id, descriptor FROM user_catalog_descriptors WHERE user_id = ?'
) . all ( req . user . id ) ;
} catch { /* pre-v0.96 */ }
const userDescsByCatalogId = new Map ( ) ;
for ( const d of userDescriptors ) {
if ( ! userDescsByCatalogId . has ( d . catalog_id ) ) userDescsByCatalogId . set ( d . catalog_id , [ ] ) ;
userDescsByCatalogId . get ( d . catalog_id ) . push ( { id : d.id , descriptor : d.descriptor } ) ;
}
const catalog = catalogEntries . map ( entry = > {
const bill = billByCatalogId . get ( entry . id ) ? ? null ;
return {
. . . entry ,
matched_bill : bill ? {
id : bill.id ,
name : bill.name ,
2026-06-11 20:12:31 -05:00
expected_amount : fromCents ( bill . expected_amount ) ,
2026-06-06 20:02:13 -05:00
active : ! ! bill . active ,
2026-06-11 20:12:31 -05:00
monthly_equivalent : monthlyEquivalent ( fromCents ( bill . expected_amount ) , bill . cycle_type , bill . billing_cycle ) ,
2026-06-06 20:02:13 -05:00
} : null ,
user_descriptors : userDescsByCatalogId.get ( entry . id ) ? ? [ ] ,
} ;
} ) ;
res . json ( { catalog } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to load catalog' , 'CATALOG_ERROR' ) ) ;
}
} ) ;
// Update which catalog entry a bill is linked to (or unlink with null)
2026-07-06 11:26:50 -05:00
router . put ( '/:id/catalog-link' , ( req : Req , res : Res ) = > {
2026-06-06 20:02:13 -05:00
const db = getDb ( ) ;
const billId = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( billId ) || billId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid bill ID' , 'VALIDATION_ERROR' ) ) ;
}
const bill = db . prepare (
2026-06-06 21:15:08 -05:00
'SELECT id, name, catalog_id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL'
2026-06-06 20:02:13 -05:00
) . get ( billId , req . user . id ) ;
if ( ! bill ) return res . status ( 404 ) . json ( standardizeError ( 'Bill not found' , 'NOT_FOUND' ) ) ;
const rawCatalogId = req . body ? . catalog_id ;
if ( rawCatalogId === null || rawCatalogId === undefined ) {
db . prepare ( "UPDATE bills SET catalog_id = NULL, updated_at = datetime('now') WHERE id = ? AND user_id = ?" )
. run ( billId , req . user . id ) ;
2026-06-06 21:15:08 -05:00
recordSubscriptionFeedback ( db , req . user . id , {
action : 'catalog_unlink' ,
catalog_id : bill.catalog_id ,
bill_id : billId ,
merchant : bill.name ,
} ) ;
2026-06-06 20:02:13 -05:00
return res . json ( { ok : true , catalog_id : null } ) ;
}
const catalogId = parseInt ( rawCatalogId , 10 ) ;
if ( ! Number . isInteger ( catalogId ) || catalogId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'catalog_id must be a positive integer or null' , 'VALIDATION_ERROR' , 'catalog_id' ) ) ;
}
const catalogEntry = db . prepare ( 'SELECT id FROM subscription_catalog WHERE id = ?' ) . get ( catalogId ) ;
if ( ! catalogEntry ) return res . status ( 404 ) . json ( standardizeError ( 'Catalog entry not found' , 'NOT_FOUND' , 'catalog_id' ) ) ;
db . prepare ( "UPDATE bills SET catalog_id = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?" )
. run ( catalogId , billId , req . user . id ) ;
2026-06-06 21:15:08 -05:00
recordSubscriptionFeedback ( db , req . user . id , {
action : 'catalog_relink' ,
catalog_id : catalogId ,
bill_id : billId ,
merchant : bill.name ,
metadata : { previous_catalog_id : bill.catalog_id || null } ,
} ) ;
2026-06-06 20:02:13 -05:00
res . json ( { ok : true , catalog_id : catalogId } ) ;
} ) ;
// Add a custom bank descriptor for a catalog entry (per-user)
2026-07-06 11:26:50 -05:00
router . post ( '/catalog/:catalogId/descriptors' , ( req : Req , res : Res ) = > {
2026-06-06 20:02:13 -05:00
const db = getDb ( ) ;
const catalogId = parseInt ( req . params . catalogId , 10 ) ;
if ( ! Number . isInteger ( catalogId ) || catalogId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid catalog ID' , 'VALIDATION_ERROR' ) ) ;
}
const catalogEntry = db . prepare ( 'SELECT id FROM subscription_catalog WHERE id = ?' ) . get ( catalogId ) ;
if ( ! catalogEntry ) return res . status ( 404 ) . json ( standardizeError ( 'Catalog entry not found' , 'NOT_FOUND' ) ) ;
const descriptor = String ( req . body ? . descriptor ? ? '' ) . trim ( ) ;
if ( ! descriptor ) {
return res . status ( 400 ) . json ( standardizeError ( 'descriptor is required' , 'VALIDATION_ERROR' , 'descriptor' ) ) ;
}
if ( descriptor . length > 100 ) {
return res . status ( 400 ) . json ( standardizeError ( 'descriptor must be 100 characters or less' , 'VALIDATION_ERROR' , 'descriptor' ) ) ;
}
try {
// Check for case-insensitive duplicate
const exists = db . prepare (
'SELECT id FROM user_catalog_descriptors WHERE user_id = ? AND catalog_id = ? AND LOWER(descriptor) = LOWER(?)'
) . get ( req . user . id , catalogId , descriptor ) ;
if ( exists ) {
return res . status ( 409 ) . json ( standardizeError ( 'Descriptor already exists for this service' , 'DUPLICATE_ERROR' , 'descriptor' ) ) ;
}
const result = db . prepare (
'INSERT INTO user_catalog_descriptors (user_id, catalog_id, descriptor) VALUES (?, ?, ?)'
) . run ( req . user . id , catalogId , descriptor ) ;
2026-06-06 21:15:08 -05:00
recordSubscriptionFeedback ( db , req . user . id , {
action : 'add_descriptor' ,
catalog_id : catalogId ,
merchant : descriptor ,
descriptor ,
} ) ;
2026-06-06 20:02:13 -05:00
res . status ( 201 ) . json ( { id : result.lastInsertRowid , descriptor , catalog_id : catalogId } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to add descriptor' , 'DESCRIPTOR_ADD_ERROR' ) ) ;
}
} ) ;
// Delete a user-added catalog descriptor
2026-07-06 11:26:50 -05:00
router . delete ( '/catalog/descriptors/:id' , ( req : Req , res : Res ) = > {
2026-06-06 20:02:13 -05:00
const db = getDb ( ) ;
const descriptorId = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( descriptorId ) || descriptorId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid descriptor ID' , 'VALIDATION_ERROR' ) ) ;
}
try {
2026-06-06 21:15:08 -05:00
const existing = db . prepare (
'SELECT catalog_id, descriptor FROM user_catalog_descriptors WHERE id = ? AND user_id = ?'
) . get ( descriptorId , req . user . id ) ;
2026-06-06 20:02:13 -05:00
const result = db . prepare (
'DELETE FROM user_catalog_descriptors WHERE id = ? AND user_id = ?'
) . run ( descriptorId , req . user . id ) ;
if ( result . changes === 0 ) {
return res . status ( 404 ) . json ( standardizeError ( 'Descriptor not found' , 'NOT_FOUND' ) ) ;
}
2026-06-06 21:15:08 -05:00
if ( existing ) {
recordSubscriptionFeedback ( db , req . user . id , {
action : 'delete_descriptor' ,
catalog_id : existing.catalog_id ,
merchant : existing.descriptor ,
descriptor : existing.descriptor ,
} ) ;
}
2026-06-06 20:02:13 -05:00
res . json ( { ok : true } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to delete descriptor' , 'DESCRIPTOR_DELETE_ERROR' ) ) ;
}
} ) ;
2026-07-06 11:26:50 -05:00
router . patch ( '/:id' , ( req : Req , res : Res ) = > {
2026-05-28 22:54:07 -05:00
const db = getDb ( ) ;
const billId = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( billId ) ) {
return res . status ( 400 ) . json ( standardizeError ( 'bill_id must be an integer' , 'VALIDATION_ERROR' , 'bill_id' ) ) ;
}
const existing = db . prepare ( 'SELECT * FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( billId , req . user . id ) ;
if ( ! existing ) return res . status ( 404 ) . json ( standardizeError ( 'Bill not found' , 'NOT_FOUND' , 'bill_id' ) ) ;
const allowedTypes = new Set ( [ 'streaming' , 'software' , 'cloud' , 'music' , 'news' , 'fitness' , 'gaming' , 'utilities' , 'insurance' , 'other' ] ) ;
const next = {
is_subscription : req.body.is_subscription !== undefined ? ( req . body . is_subscription ? 1 : 0 ) : existing . is_subscription ,
subscription_type : req.body.subscription_type !== undefined
? ( allowedTypes . has ( req . body . subscription_type ) ? req . body . subscription_type : 'other' )
: existing . subscription_type ,
reminder_days_before : req.body.reminder_days_before !== undefined
? Number ( req . body . reminder_days_before )
: existing . reminder_days_before ,
active : req.body.active !== undefined ? ( req . body . active ? 1 : 0 ) : existing . active ,
} ;
if ( ! Number . isInteger ( next . reminder_days_before ) || next . reminder_days_before < 0 || next . reminder_days_before > 30 ) {
return res . status ( 400 ) . json ( standardizeError ( 'reminder_days_before must be between 0 and 30' , 'VALIDATION_ERROR' , 'reminder_days_before' ) ) ;
}
db . prepare ( `
UPDATE bills
SET is_subscription = ? ,
subscription_type = ? ,
reminder_days_before = ? ,
active = ? ,
updated_at = datetime ( 'now' )
WHERE id = ? AND user_id = ?
` ).run(next.is_subscription, next.subscription_type, next.reminder_days_before, next.active, billId, req.user.id);
const updated = db . prepare ( `
SELECT b . * , c . name AS category_name
FROM bills b
LEFT JOIN categories c ON c . id = b . category_id AND c . user_id = b . user_id AND c . deleted_at IS NULL
WHERE b . id = ? AND b . user_id = ?
` ).get(billId, req.user.id);
res . json ( decorateSubscription ( updated ) ) ;
} ) ;
module .exports = router ;