Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 1x 1x 1x | import { CategoriesService, TransactionRead, TransactionsService, TransactionTypeProperty } from "@billos/firefly-iii-sdk"
import { Temporal } from "@js-temporal/polyfill"
import pino from "pino"
import { client } from "../../client"
import { env } from "../../config"
import DynamicConfig, { AConfig, VConfig } from "../../modules/config/dynamic"
import { getNotifier } from "../../modules/notifiers"
import { getBudgetName } from "../../utils/budgetName"
import { bindTransactionToNotification } from "../../utils/notification"
import { renderTemplate, TemplateName } from "../../utils/renderTemplate"
import { addTransactionJobToQueue } from "../utils"
import { TransactionJob } from "./BaseJob"
const logger = pino()
async function getUncategorizedTransactions(start?: string, end?: string): Promise<TransactionRead[]> {
const transactions: TransactionRead[] = []
const {
meta: { pagination: { total_pages = 1 } = { total_pages: 1 } },
} = await TransactionsService.listTransaction({ client, query: { page: 1, limit: 200, start, end } })
for (let page = 1; page <= total_pages; page++) {
const { data } = await TransactionsService.listTransaction({ client, query: { page, limit: 200, start, end } })
const filteredData = data.filter(
(transaction) =>
!transaction.attributes.transactions[0].category_id &&
transaction.attributes.transactions[0].type === TransactionTypeProperty.WITHDRAWAL,
)
transactions.push(...filteredData)
}
return transactions
}
export class UncategorizedTransactionsJob extends TransactionJob {
readonly id = "uncategorized-transactions"
override readonly startDelay = 10
async run(id: string): Promise<void> {
logger.info("Checking that transaction %s exists", id)
try {
await TransactionsService.getTransaction({ client, path: { id } })
} catch {
logger.error("Transaction %s does not exist", id)
return
}
logger.info("Creating a new message for uncategorized transaction with key %s", id)
const {
data: {
attributes: {
transactions: [transaction],
},
},
} = await TransactionsService.getTransaction({ client, path: { id } })
// Ensure the transaction is a withdrawal
const { type } = transaction
if (type !== TransactionTypeProperty.WITHDRAWAL) {
logger.info("Transaction %s is not a withdrawal", id)
return
}
if (!transaction) {
logger.info("Transaction %s not found", id)
return
}
if (transaction.category_id) {
logger.info("Transaction %s already categorized", id)
return
}
const billsBudgetId = await DynamicConfig.get(VConfig.RoleBudgetBillsId)
if (!billsBudgetId) {
logger.error("Bills budget ID is not configured. Please set it in the environment variables or in Redis.")
return
}
const billsBudgetName = await getBudgetName(billsBudgetId)
const { data: allCategories } = await CategoriesService.listCategory({ client, query: { page: 1, limit: 50 } })
const hiddenCategoriesSet = new Set(await DynamicConfig.lrange(AConfig.HiddenCategories, 0, -1))
const categories = allCategories.filter(({ attributes: { name } }) => name !== billsBudgetName && !hiddenCategoriesSet.has(name))
const groupSize = 3
const categoriesGroups = []
for (let i = 0; i < categories.length; i += groupSize) {
categoriesGroups.push(categories.slice(i, i + groupSize))
}
const notifier = await getNotifier()
if (!notifier) {
logger.warn("No notifier configured, skipping message creation for transaction %s", id)
return
}
const { title, content } = await renderTemplate(TemplateName.UncategorizedTransaction, {
transaction,
transactionId: id,
categories,
categoriesGroups,
})
const messageId = await notifier.getMessageId("CategoryMessageId", id)
if (messageId) {
const messageExists = await notifier.hasMessageId(messageId)
if (messageExists) {
logger.info("Category message already exists for transaction %s", id)
return
}
logger.info("Category message defined but not found in notifier for transaction %s", id)
}
const newMessageId = await notifier.sendMessage(title, content)
await bindTransactionToNotification(id, "CategoryMessageId", newMessageId)
}
override async init(): Promise<void> {
logger.info("Initializing UnbudgetedTransactions jobs for all unbudgeted transactions")
const notifier = await getNotifier()
if (notifier) {
const startDate = Temporal.Now.zonedDateTimeISO(env.timezone).subtract({ months: 3 }).startOfDay()
const start = startDate.toPlainDate().toString()
const endDate = Temporal.Now.zonedDateTimeISO(env.timezone)
const end = endDate.toPlainDate().toString()
if (!end) {
logger.error("Failed to get current date in ISO format")
return
}
const uncategorizedTransactionsList = await getUncategorizedTransactions(start, end)
for (const { id: transactionId } of uncategorizedTransactionsList) {
await addTransactionJobToQueue(this, transactionId)
}
}
logger.info("Initialized UnbudgetedTransactions jobs for %d transactions", 0)
}
}
|