Skip to content

Commit

Permalink
Merge pull request #104 from grassrootseconomics/philip/sms-address
Browse files Browse the repository at this point in the history
Sends user address of SMS; capped at 5 messages per day.
  • Loading branch information
mango-habanero authored Nov 6, 2023
2 parents 9c4ea92 + 0fd2487 commit cd61557
Show file tree
Hide file tree
Showing 14 changed files with 125 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function stringToList (value: string | undefined): string[] | void {
}

export const config = {
ADDRESS_SMS_THRESHOLD: parseInt(process.env.ADDRESS_SMS_THRESHOLD ?? '5'),
AT: {
ACTIVE: process.env.AT_ACTIVE === 'true',
API_KEY: process.env.AT_API_KEY ?? 'x',
Expand Down
1 change: 1 addition & 0 deletions src/db/models/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface AccountInterface {
activated_on_chain: boolean
activated_on_ussd: boolean
address: string
address_sms_threshold?: number
guardian: string // probably should be part of a different interface
guardians?: string[]
id: number
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/eng/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const eng_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON My Account\n1. Profile\n2. Change language\n3. Check balances\n4. Check statement\n5. PIN options\n6. My Address\n0. Back',
displayAddress: 'CON Address:\n{address}\n0. Back',
displayAddress: 'END Address:\n{address}',
}

export default eng_settings
2 changes: 1 addition & 1 deletion src/i18n/fra/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const fra_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Mon compte\n1. Mon profil\n2. Changer langue\n3. Verifier solde\n4. Dernieres transactions\n5. Options PIN\n6. Mon adresse\n0. Retour',
displayAddress: 'CON Adresse:\n{address}\n0. Retour',
displayAddress: 'END Adresse:\n{address}',
}

export default fra_settings
2 changes: 1 addition & 1 deletion src/i18n/gax/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const gax_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Akaunti khiy\n1. Profile khiy\n2. Afaan chaguad\n3. Balansi laalad\n4. Odhuu jalkaba laal\n5. Pin ta atin feet\n6. Adresi khiy\n0. Dheebi',
displayAddress: 'CON Adresi:\n{address}\n0. Dheebi',
displayAddress: 'END Adresi:\n{address}',
}

export default gax_settings
8 changes: 3 additions & 5 deletions src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,8 @@ export type NamespaceSettingsTranslation = {
*/
settingsMenu: string
/**
* C​O​N​ ​A​n​w​a​n​i​:​
​{​a​d​d​r​e​s​s​}​
​0​.​ ​R​u​d​i
* E​N​D​ ​A​n​w​a​n​i​:​
​{​a​d​d​r​e​s​s​}
* @param {unknown} address
*/
displayAddress: RequiredParams<'address'>
Expand Down Expand Up @@ -1625,9 +1624,8 @@ export type TranslationFunctions = {
*/
settingsMenu: () => LocalizedString
/**
* CON Anwani:
* END Anwani:
{address}
0. Rudi
*/
displayAddress: (arg: { address: unknown }) => LocalizedString
}
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/kam/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const kam_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Kinandu chakwa\n1. Welesyo wakwa\n2. Sakua luka\n3. Sisya mbalansi\n4. Sisya welesyo\n5. Welesyo wa PIN\n6. Adresi yakwa\n0. Syoka',
displayAddress: 'CON Adresi:\n{address}\n0. Syoka',
displayAddress: 'END Adresi:\n{address}',
}

export default kam_settings
2 changes: 1 addition & 1 deletion src/i18n/kik/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const kik_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Akaunti yakwa\n1. Maritwa makwa\n2. Cagura rothiumu\n3. Rora matigari\n4. Rora rugano\n5. Cenjia PIN\n6. Adresi yakwa\n0. Coka',
displayAddress: 'CON Adresi:\n{address}\n0. Coka',
displayAddress: 'END Adresi:\n{address}',
}

export default kik_settings
2 changes: 1 addition & 1 deletion src/i18n/luo/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const luo_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Akaont na\n1. Nyanonro mara\n2. Yier dhok\n3. Ngi midong go\n4. N\'gi chal\n5. Lok PIN\n6. Adresi na\n0. Chien',
displayAddress: 'CON Adresi:\n{address}\n0. Chien',
displayAddress: 'END Adresi:\n{address}',
}

export default luo_settings
2 changes: 1 addition & 1 deletion src/i18n/mij/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const mij_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Akaunti yangu\n1. Malagizo gangu\n2. Tsagula luga\n3. Lola sazo\n4. Lola tarifa\n5. Galuza PIN\n6. Adilesi yangu\n0. Uya Nyuma',
displayAddress: 'CON Adilesi:\n{address}\n0. Uya Nyuma',
displayAddress: 'END Adilesi:\n{address}',
}

export default mij_settings
2 changes: 1 addition & 1 deletion src/i18n/swa/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const swa_settings: NamespaceSettingsTranslation = {
mainMenu: mainMenu,
settingsMenu:
'CON Akaunti yangu\n1. Wasifu wangu\n2. Badili lugha\n3. Angalia salio\n4. Taarifa ya matumizi\n5. Mipangilio ya PIN\n6. Anwani yangu\n0. Rudi',
displayAddress: 'CON Anwani:\n{address}\n0. Rudi',
displayAddress: 'END Anwani:\n{address}',

}

Expand Down
99 changes: 82 additions & 17 deletions src/machines/settings.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { createMachine } from 'xstate';
import { L } from '@i18n/i18n-node';
import {PostgresDb} from "@fastify/postgres";
import { Redis as RedisClient } from 'ioredis';
import {
intermediateMachineTranslations,
isOption1,
isOption2,
isOption3,
isOption4,
isOption5,
isOption6,
MachineEvent,
MachineId,
MachineInterface,
UserContext
intermediateMachineTranslations,
isOption1,
isOption2,
isOption3,
isOption4,
isOption5,
isOption6,
isSuccess,
MachineEvent,
MachineId,
MachineInterface, NotifierContext
} from '@machines/utils';
import {translate} from "@i18n/translators";
import {sendSMS} from "@lib/ussd";
import {AccountService} from "@services/account";
import {config} from "@/config";

const stateMachine = createMachine<UserContext, MachineEvent>({

export interface SettingsContext extends NotifierContext {}

const stateMachine = createMachine<SettingsContext, MachineEvent>({
id: MachineId.SETTINGS,
initial: "settingsMenu",
states: {
Expand All @@ -23,9 +33,8 @@ const stateMachine = createMachine<UserContext, MachineEvent>({
},
displayAddress: {
description: 'Displays the address of the current user.',
on: {
BACK: 'settingsMenu'
}
type: 'final',
tags: 'resolved'
},
language: {
description: 'Transitions to language machine',
Expand All @@ -43,6 +52,16 @@ const stateMachine = createMachine<UserContext, MachineEvent>({
description: 'Transitions to profile machine',
type: 'final'
},
sendingAddress: {
description: 'Sends user address to the user via SMS.',
invoke: {
id: 'sendingAddress',
src: 'smsAddress',
onDone: { target: 'displayAddress', cond: 'isSuccess' },
onError: { target: 'displayAddress' }
},
tags: 'invoked'
},
settingsMenu: {
description: 'Displays account management menu.',
on: {
Expand All @@ -53,7 +72,7 @@ const stateMachine = createMachine<UserContext, MachineEvent>({
{ target: 'balances', cond: 'isOption3' },
{ target: 'statement', cond: 'isOption4' },
{ target: 'pinManagement', cond: 'isOption5' },
{ target: 'displayAddress', cond: 'isOption6'}
{ target: 'sendingAddress', cond: 'isOption6'}
]
}
},
Expand All @@ -69,11 +88,57 @@ const stateMachine = createMachine<UserContext, MachineEvent>({
isOption3,
isOption4,
isOption5,
isOption6
isOption6,
isSuccess
},
services: {
smsAddress
}
})

export const settingsMachine: MachineInterface = {
stateMachine,
translate: intermediateMachineTranslations
}


async function updateAddressSmsThreshold(db: PostgresDb, redis: RedisClient, phoneNumber: string, threshold: number) {
const thresholdKey = `address-sms-threshold-reset-${phoneNumber}`;
const lastReset = await redis.get(thresholdKey);
const thresholdResetTime = lastReset ? parseInt(lastReset, 10) : Date.now();

if (Date.now() - thresholdResetTime > 86400000 || !lastReset) {
threshold = 1;
await redis.set(thresholdKey, Date.now().toString());
}

await new AccountService(db, redis).updateAddressSmsThreshold(threshold, phoneNumber);
}


async function smsAddress(context: SettingsContext) {
const {
connections: { db, redis},
notifier,
user: { account: { address, address_sms_threshold, language, phone_number }
} } = context;

if (address_sms_threshold && address_sms_threshold >= config.ADDRESS_SMS_THRESHOLD){
console.warn(`SMS address threshold reached for ${phone_number}`);
return { success: true };
}

const translator = L[language][MachineId.SETTINGS];
let message: string = await translate('displayAddress', translator, { address });

// remove END from message.
message = message.replace(/END /g, '')

// replace new line with space for SMS.
message = message.replace(/\n/g, ' ');

await sendSMS(message, notifier, [phone_number]);
const threshold = address_sms_threshold ? address_sms_threshold + 1 : 1;
await updateAddressSmsThreshold(db, redis.persistent, phone_number, threshold);
return { success: true };
}
4 changes: 4 additions & 0 deletions src/services/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export class AccountService {
await new UserService(phoneNumber, this.redis).update(data)
}

public async updateAddressSmsThreshold(addressSmsThreshold: number, phoneNumber: string) {
await this.updateCache(phoneNumber, { account: { address_sms_threshold: addressSmsThreshold } })
}

public async updateLanguage(address: string, graphql: GraphQLClient, graphUserId: number, language: Locales, phoneNumber: string) {
const results = await Promise.allSettled([
new Account(this.db).setLanguage(phoneNumber, language),
Expand Down
51 changes: 26 additions & 25 deletions src/services/machine.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { SessionInterface, SessionType } from '@db/models/session';
import { RegistrationContext, registrationMachine } from '@machines/registration';
import { AccountStatus } from '@db/models/account';
import { AuthContext, authMachine, hashValue } from '@machines/auth';
import { mainMachine } from '@machines/main';
import { SessionService } from '@services/session';
import { interpret, Interpreter, StateValue } from 'xstate';
import { fallbackLanguage, languageOptions, tFeedback } from '@i18n/translators';
import { BalancesContext, balancesMachine } from '@machines/balances';
import { LanguagesContext, languagesMachine } from '@machines/languages';
import { PinManagementContext, pinManagementMachine } from '@machines/pins';
import { ProfileContext, profileMachine } from '@machines/profile';
import { settingsMachine } from '@machines/settings';
import { SocialRecoveryContext, socialRecoveryMachine } from '@machines/socialRecovery';
import { StatementContext, statementMachine } from '@machines/statement';
import { TransferContext, transferMachine } from '@machines/transfer';
import { voucherMachine, VouchersContext } from '@machines/voucher';
import { SystemError } from '@lib/errors';
import { waitFor } from 'xstate/lib/waitFor';
import { L } from '@i18n/i18n-node';
import { Locales, NamespaceFeedbackTranslation } from '@i18n/i18n-types';
import {SessionInterface, SessionType} from '@db/models/session';
import {RegistrationContext, registrationMachine} from '@machines/registration';
import {AccountStatus} from '@db/models/account';
import {AuthContext, authMachine, hashValue} from '@machines/auth';
import {mainMachine} from '@machines/main';
import {SessionService} from '@services/session';
import {interpret, Interpreter, StateValue} from 'xstate';
import {fallbackLanguage, languageOptions, tFeedback} from '@i18n/translators';
import {BalancesContext, balancesMachine} from '@machines/balances';
import {LanguagesContext, languagesMachine} from '@machines/languages';
import {PinManagementContext, pinManagementMachine} from '@machines/pins';
import {ProfileContext, profileMachine} from '@machines/profile';
import {SettingsContext, settingsMachine} from '@machines/settings';
import {SocialRecoveryContext, socialRecoveryMachine} from '@machines/socialRecovery';
import {StatementContext, statementMachine} from '@machines/statement';
import {TransferContext, transferMachine} from '@machines/transfer';
import {voucherMachine, VouchersContext} from '@machines/voucher';
import {SystemError} from '@lib/errors';
import {waitFor} from 'xstate/lib/waitFor';
import {L} from '@i18n/i18n-node';
import {Locales, NamespaceFeedbackTranslation} from '@i18n/i18n-types';
import {
BaseContext,
MachineEvent,
Expand All @@ -28,9 +28,9 @@ import {
NotifierContext,
UserContext
} from '@machines/utils';
import { ActorMenu, mainMenu, pinManagementMenu, settingsMenu } from '@lib/menus';
import { LocalizedString } from 'typesafe-i18n';
import { config } from '@/config';
import {ActorMenu, mainMenu, pinManagementMenu, settingsMenu} from '@lib/menus';
import {LocalizedString} from 'typesafe-i18n';
import {config} from '@/config';

export type MachineContext =
| AuthContext
Expand All @@ -41,6 +41,7 @@ export type MachineContext =
| PinManagementContext
| ProfileContext
| RegistrationContext
| SettingsContext
| SocialRecoveryContext
| StatementContext
| TransferContext
Expand All @@ -63,7 +64,7 @@ export const machines: MachineInterface[] = [
]

// TODO[Philip]: "Notifiable" can be a machine attribute to insert the notifier to context in a cleaner way.
export const notifyingMachines: MachineId[] = [MachineId.PIN_MANAGEMENT, MachineId.TRANSFER]
export const notifyingMachines: MachineId[] = [MachineId.PIN_MANAGEMENT, MachineId.SETTINGS, MachineId.TRANSFER]

class MachineService implements MachineServiceInterface {

Expand Down

0 comments on commit cd61557

Please sign in to comment.