mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 17:44:03 +00:00
Fix purchase topic engagement gating
This commit is contained in:
@@ -48,7 +48,12 @@ function participants() {
|
|||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
function purchaseUpdate(text: string) {
|
function purchaseUpdate(
|
||||||
|
text: string,
|
||||||
|
options: {
|
||||||
|
replyToBot?: boolean
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
const commandToken = text.split(' ')[0] ?? text
|
const commandToken = text.split(' ')[0] ?? text
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -67,6 +72,25 @@ function purchaseUpdate(text: string) {
|
|||||||
is_bot: false,
|
is_bot: false,
|
||||||
first_name: 'Mia'
|
first_name: 'Mia'
|
||||||
},
|
},
|
||||||
|
...(options.replyToBot
|
||||||
|
? {
|
||||||
|
reply_to_message: {
|
||||||
|
message_id: 12,
|
||||||
|
date: Math.floor(Date.now() / 1000),
|
||||||
|
chat: {
|
||||||
|
id: Number(config.householdChatId),
|
||||||
|
type: 'supergroup'
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
id: 999000,
|
||||||
|
is_bot: true,
|
||||||
|
first_name: 'Household Test Bot',
|
||||||
|
username: 'household_test_bot'
|
||||||
|
},
|
||||||
|
text: 'Which amount was that purchase?'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
text,
|
text,
|
||||||
entities: text.startsWith('/')
|
entities: text.startsWith('/')
|
||||||
? [
|
? [
|
||||||
@@ -440,6 +464,94 @@ Confirm or cancel below.`,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('keeps bare-amount purchase reports on the ingestion path', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
|
||||||
|
if (method === 'sendMessage') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
message_id: calls.length,
|
||||||
|
date: Math.floor(Date.now() / 1000),
|
||||||
|
chat: {
|
||||||
|
id: Number(config.householdChatId),
|
||||||
|
type: 'supergroup'
|
||||||
|
},
|
||||||
|
text: (payload as { text?: string }).text ?? 'ok'
|
||||||
|
}
|
||||||
|
} as never
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: true
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async save(record) {
|
||||||
|
expect(record.rawText).toBe('Bought toilet paper 30')
|
||||||
|
return {
|
||||||
|
status: 'clarification_needed',
|
||||||
|
purchaseMessageId: 'proposal-amount-only',
|
||||||
|
clarificationQuestion: 'Which currency was this purchase in?',
|
||||||
|
parsedAmountMinor: 3000n,
|
||||||
|
parsedCurrency: null,
|
||||||
|
parsedItemDescription: 'toilet paper',
|
||||||
|
parserConfidence: 58,
|
||||||
|
parserMode: 'llm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'clarification',
|
||||||
|
amountMinor: 3000n,
|
||||||
|
currency: null,
|
||||||
|
itemDescription: 'toilet paper',
|
||||||
|
confidence: 58,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: 'Which currency was this purchase in?'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('Bought toilet paper 30') as never)
|
||||||
|
|
||||||
|
expect(calls).toHaveLength(3)
|
||||||
|
expect(calls[0]).toMatchObject({
|
||||||
|
method: 'sendChatAction'
|
||||||
|
})
|
||||||
|
expect(calls[1]).toMatchObject({
|
||||||
|
method: 'sendMessage',
|
||||||
|
payload: {
|
||||||
|
text: 'Checking that purchase...'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(calls[2]).toMatchObject({
|
||||||
|
method: 'editMessageText',
|
||||||
|
payload: {
|
||||||
|
text: 'Which currency was this purchase in?'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('sends a processing reply and edits it when an interpreter is configured', async () => {
|
test('sends a processing reply and edits it when an interpreter is configured', async () => {
|
||||||
const bot = createTestBot()
|
const bot = createTestBot()
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
@@ -569,6 +681,112 @@ Confirm or cancel below.`,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('stays silent for planning chatter even when an interpreter is configured', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
let saveCalls = 0
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: true
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
saveCalls += 1
|
||||||
|
return {
|
||||||
|
status: 'ignored_not_purchase',
|
||||||
|
purchaseMessageId: 'ignored-1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'not_purchase',
|
||||||
|
amountMinor: null,
|
||||||
|
currency: null,
|
||||||
|
itemDescription: null,
|
||||||
|
confidence: 12,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('We should buy toilet paper for 30 gel') as never)
|
||||||
|
|
||||||
|
expect(saveCalls).toBe(0)
|
||||||
|
expect(calls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('stays silent for stray amount chatter in the purchase topic', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
let saveCalls = 0
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: true
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
saveCalls += 1
|
||||||
|
return {
|
||||||
|
status: 'ignored_not_purchase',
|
||||||
|
purchaseMessageId: 'ignored-2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'not_purchase',
|
||||||
|
amountMinor: null,
|
||||||
|
currency: null,
|
||||||
|
itemDescription: null,
|
||||||
|
confidence: 17,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('This machine costs 300 gel, scary') as never)
|
||||||
|
|
||||||
|
expect(saveCalls).toBe(0)
|
||||||
|
expect(calls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
test('does not reply for duplicate deliveries or non-purchase chatter', async () => {
|
test('does not reply for duplicate deliveries or non-purchase chatter', async () => {
|
||||||
const bot = createTestBot()
|
const bot = createTestBot()
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
@@ -716,6 +934,215 @@ Confirm or cancel below.`
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('does not send the purchase handoff for tagged non-purchase conversation', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
let saveCalls = 0
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: true
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
saveCalls += 1
|
||||||
|
return {
|
||||||
|
status: 'ignored_not_purchase',
|
||||||
|
purchaseMessageId: 'ignored-3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'not_purchase',
|
||||||
|
amountMinor: null,
|
||||||
|
currency: null,
|
||||||
|
itemDescription: null,
|
||||||
|
confidence: 19,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('@household_test_bot please ignore me today') as never)
|
||||||
|
|
||||||
|
expect(saveCalls).toBe(1)
|
||||||
|
expect(calls).toHaveLength(1)
|
||||||
|
expect(calls[0]).toMatchObject({
|
||||||
|
method: 'sendChatAction',
|
||||||
|
payload: {
|
||||||
|
chat_id: Number(config.householdChatId),
|
||||||
|
action: 'typing',
|
||||||
|
message_thread_id: config.purchaseTopicId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('continues purchase handling for replies to bot messages without a fresh mention', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
message_id: calls.length,
|
||||||
|
date: Math.floor(Date.now() / 1000),
|
||||||
|
chat: {
|
||||||
|
id: Number(config.householdChatId),
|
||||||
|
type: 'supergroup'
|
||||||
|
},
|
||||||
|
text: 'ok'
|
||||||
|
}
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async save(record) {
|
||||||
|
expect(record.rawText).toBe('Actually it was 32 gel')
|
||||||
|
return {
|
||||||
|
status: 'clarification_needed',
|
||||||
|
purchaseMessageId: 'proposal-2',
|
||||||
|
clarificationQuestion: 'Was that for toilet paper?',
|
||||||
|
parsedAmountMinor: 3200n,
|
||||||
|
parsedCurrency: 'GEL',
|
||||||
|
parsedItemDescription: null,
|
||||||
|
parserConfidence: 61,
|
||||||
|
parserMode: 'llm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'clarification',
|
||||||
|
amountMinor: 3200n,
|
||||||
|
currency: 'GEL',
|
||||||
|
itemDescription: null,
|
||||||
|
confidence: 61,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: 'Was that for toilet paper?'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('Actually it was 32 gel', { replyToBot: true }) as never)
|
||||||
|
|
||||||
|
expect(calls).toHaveLength(2)
|
||||||
|
expect(calls[0]).toMatchObject({
|
||||||
|
method: 'sendChatAction'
|
||||||
|
})
|
||||||
|
expect(calls[1]).toMatchObject({
|
||||||
|
method: 'sendMessage',
|
||||||
|
payload: {
|
||||||
|
text: 'Was that for toilet paper?'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('continues purchase handling for active clarification context without a fresh mention', async () => {
|
||||||
|
const bot = createTestBot()
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
message_id: calls.length,
|
||||||
|
date: Math.floor(Date.now() / 1000),
|
||||||
|
chat: {
|
||||||
|
id: Number(config.householdChatId),
|
||||||
|
type: 'supergroup'
|
||||||
|
},
|
||||||
|
text: 'ok'
|
||||||
|
}
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const repository: PurchaseMessageIngestionRepository = {
|
||||||
|
async hasClarificationContext() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
async save(record) {
|
||||||
|
expect(record.rawText).toBe('32 gel')
|
||||||
|
return {
|
||||||
|
status: 'clarification_needed',
|
||||||
|
purchaseMessageId: 'proposal-3',
|
||||||
|
clarificationQuestion: 'What item was that for?',
|
||||||
|
parsedAmountMinor: 3200n,
|
||||||
|
parsedCurrency: 'GEL',
|
||||||
|
parsedItemDescription: null,
|
||||||
|
parserConfidence: 58,
|
||||||
|
parserMode: 'llm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirm() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async toggleParticipant() {
|
||||||
|
throw new Error('not used')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPurchaseTopicIngestion(bot, config, repository, {
|
||||||
|
interpreter: async () => ({
|
||||||
|
decision: 'clarification',
|
||||||
|
amountMinor: 3200n,
|
||||||
|
currency: 'GEL',
|
||||||
|
itemDescription: null,
|
||||||
|
confidence: 58,
|
||||||
|
parserMode: 'llm',
|
||||||
|
clarificationQuestion: 'What item was that for?'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(purchaseUpdate('32 gel') as never)
|
||||||
|
|
||||||
|
expect(calls).toHaveLength(2)
|
||||||
|
expect(calls[0]).toMatchObject({
|
||||||
|
method: 'sendChatAction'
|
||||||
|
})
|
||||||
|
expect(calls[1]).toMatchObject({
|
||||||
|
method: 'sendMessage',
|
||||||
|
payload: {
|
||||||
|
text: 'What item was that for?'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('toggles purchase participants before confirmation', async () => {
|
test('toggles purchase participants before confirmation', async () => {
|
||||||
const bot = createTestBot()
|
const bot = createTestBot()
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
|||||||
@@ -20,6 +20,27 @@ const PURCHASE_CONFIRM_CALLBACK_PREFIX = 'purchase:confirm:'
|
|||||||
const PURCHASE_CANCEL_CALLBACK_PREFIX = 'purchase:cancel:'
|
const PURCHASE_CANCEL_CALLBACK_PREFIX = 'purchase:cancel:'
|
||||||
const PURCHASE_PARTICIPANT_CALLBACK_PREFIX = 'purchase:participant:'
|
const PURCHASE_PARTICIPANT_CALLBACK_PREFIX = 'purchase:participant:'
|
||||||
const MIN_PROPOSAL_CONFIDENCE = 70
|
const MIN_PROPOSAL_CONFIDENCE = 70
|
||||||
|
const LIKELY_PURCHASE_VERB_PATTERN =
|
||||||
|
/\b(?:bought|purchased|paid|spent|ordered|picked up|grabbed|got)\b|\b(?:купил(?:а|и)?|куплено|заказал(?:а|и)?|оплатил(?:а|и)?|потратил(?:а|и)?|взял(?:а|и)?)\b/iu
|
||||||
|
const PLANNING_PURCHASE_PATTERN =
|
||||||
|
/\b(?:should buy|should get|need to buy|need to get|want to buy|want to get|let'?s buy|let'?s get|going to buy|gonna buy|plan to buy|planning to buy|thinking about buying|thinking of buying|should we buy|should we get|can buy)\b|\b(?:надо|нужно|хочу|хотим|давай(?:те)?|будем|планирую|планируем|может|стоит)\s+(?:купить|взять|заказать|оплатить)\b|\b(?:купим|возьмем|возьмём|закажем|оплатим)\b/iu
|
||||||
|
const MONEY_SIGNAL_PATTERN =
|
||||||
|
/\b\d+(?:[.,]\d{1,2})?\s*(?:₾|gel|lari|лари|tetri|тетри|usd|\$|доллар(?:а|ов)?)\b|\b(?:for|за|на)\s+\d+(?:[.,]\d{1,2})?\b|\b(?:paid|spent|заплатил(?:а|и)?|потратил(?:а|и)?|отдал(?:а|и)?|выложил(?:а|и)?)\s+\d+(?:[.,]\d{1,2})?\b/iu
|
||||||
|
const STANDALONE_NUMBER_PATTERN = /\b\d+(?:[.,]\d{1,2})?\b/gu
|
||||||
|
|
||||||
|
type PurchaseTopicEngagement =
|
||||||
|
| {
|
||||||
|
kind: 'direct'
|
||||||
|
showProcessingReply: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'clarification'
|
||||||
|
showProcessingReply: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'likely_purchase'
|
||||||
|
showProcessingReply: true
|
||||||
|
}
|
||||||
|
|
||||||
type StoredPurchaseProcessingStatus =
|
type StoredPurchaseProcessingStatus =
|
||||||
| 'pending_confirmation'
|
| 'pending_confirmation'
|
||||||
@@ -206,6 +227,61 @@ function periodFromInstant(instant: Instant, timezone: string): string {
|
|||||||
return `${localDate.year}-${String(localDate.month).padStart(2, '0')}`
|
return `${localDate.year}-${String(localDate.month).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReplyToCurrentBot(ctx: Pick<Context, 'msg' | 'me'>): boolean {
|
||||||
|
const replyAuthor = ctx.msg?.reply_to_message?.from
|
||||||
|
if (!replyAuthor?.is_bot) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return replyAuthor.id === ctx.me.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeLikelyCompletedPurchase(rawText: string): boolean {
|
||||||
|
if (PLANNING_PURCHASE_PATTERN.test(rawText)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LIKELY_PURCHASE_VERB_PATTERN.test(rawText)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MONEY_SIGNAL_PATTERN.test(rawText)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(rawText.matchAll(STANDALONE_NUMBER_PATTERN)).length === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePurchaseTopicEngagement(
|
||||||
|
ctx: Pick<Context, 'msg' | 'me'>,
|
||||||
|
record: PurchaseTopicRecord,
|
||||||
|
repository: Pick<PurchaseMessageIngestionRepository, 'hasClarificationContext'>
|
||||||
|
): Promise<PurchaseTopicEngagement | null> {
|
||||||
|
const hasExplicitMention = stripExplicitBotMention(ctx) !== null
|
||||||
|
if (hasExplicitMention || isReplyToCurrentBot(ctx)) {
|
||||||
|
return {
|
||||||
|
kind: 'direct',
|
||||||
|
showProcessingReply: looksLikeLikelyCompletedPurchase(record.rawText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await repository.hasClarificationContext(record)) {
|
||||||
|
return {
|
||||||
|
kind: 'clarification',
|
||||||
|
showProcessingReply: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (looksLikeLikelyCompletedPurchase(record.rawText)) {
|
||||||
|
return {
|
||||||
|
kind: 'likely_purchase',
|
||||||
|
showProcessingReply: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeInterpretation(
|
function normalizeInterpretation(
|
||||||
interpretation: PurchaseInterpretation | null,
|
interpretation: PurchaseInterpretation | null,
|
||||||
parserError: string | null
|
parserError: string | null
|
||||||
@@ -1481,14 +1557,23 @@ export function registerPurchaseTopicIngestion(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const typingIndicator = options.interpreter ? startTypingIndicator(ctx) : null
|
let typingIndicator: ReturnType<typeof startTypingIndicator> | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pendingReply = options.interpreter
|
const engagement = await resolvePurchaseTopicEngagement(ctx, record, repository)
|
||||||
? await sendPurchaseProcessingReply(ctx, getBotTranslations('en').purchase.processing)
|
if (!engagement) {
|
||||||
: null
|
await next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typingIndicator = options.interpreter ? startTypingIndicator(ctx) : null
|
||||||
|
const pendingReply =
|
||||||
|
options.interpreter && engagement.showProcessingReply
|
||||||
|
? await sendPurchaseProcessingReply(ctx, getBotTranslations('en').purchase.processing)
|
||||||
|
: null
|
||||||
const result = await repository.save(record, options.interpreter, 'GEL')
|
const result = await repository.save(record, options.interpreter, 'GEL')
|
||||||
if (stripExplicitBotMention(ctx) && result.status === 'ignored_not_purchase') {
|
|
||||||
|
if (engagement.kind === 'direct' && result.status === 'ignored_not_purchase') {
|
||||||
return await next()
|
return await next()
|
||||||
}
|
}
|
||||||
await handlePurchaseMessageResult(ctx, record, result, 'en', options.logger, pendingReply)
|
await handlePurchaseMessageResult(ctx, record, result, 'en', options.logger, pendingReply)
|
||||||
@@ -1549,9 +1634,16 @@ export function registerConfiguredPurchaseTopicIngestion(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const typingIndicator = options.interpreter ? startTypingIndicator(ctx) : null
|
let typingIndicator: ReturnType<typeof startTypingIndicator> | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const engagement = await resolvePurchaseTopicEngagement(ctx, record, repository)
|
||||||
|
if (!engagement) {
|
||||||
|
await next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typingIndicator = options.interpreter ? startTypingIndicator(ctx) : null
|
||||||
const [billingSettings, assistantConfig] = await Promise.all([
|
const [billingSettings, assistantConfig] = await Promise.all([
|
||||||
householdConfigurationRepository.getHouseholdBillingSettings(record.householdId),
|
householdConfigurationRepository.getHouseholdBillingSettings(record.householdId),
|
||||||
resolveAssistantConfig(householdConfigurationRepository, record.householdId)
|
resolveAssistantConfig(householdConfigurationRepository, record.householdId)
|
||||||
@@ -1560,9 +1652,10 @@ export function registerConfiguredPurchaseTopicIngestion(
|
|||||||
householdConfigurationRepository,
|
householdConfigurationRepository,
|
||||||
record.householdId
|
record.householdId
|
||||||
)
|
)
|
||||||
const pendingReply = options.interpreter
|
const pendingReply =
|
||||||
? await sendPurchaseProcessingReply(ctx, getBotTranslations(locale).purchase.processing)
|
options.interpreter && engagement.showProcessingReply
|
||||||
: null
|
? await sendPurchaseProcessingReply(ctx, getBotTranslations(locale).purchase.processing)
|
||||||
|
: null
|
||||||
const result = await repository.save(
|
const result = await repository.save(
|
||||||
record,
|
record,
|
||||||
options.interpreter,
|
options.interpreter,
|
||||||
@@ -1572,7 +1665,7 @@ export function registerConfiguredPurchaseTopicIngestion(
|
|||||||
assistantTone: assistantConfig.assistantTone
|
assistantTone: assistantConfig.assistantTone
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (stripExplicitBotMention(ctx) && result.status === 'ignored_not_purchase') {
|
if (engagement.kind === 'direct' && result.status === 'ignored_not_purchase') {
|
||||||
return await next()
|
return await next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user