Skip to content

Commit

Permalink
feat: Add SMS utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
MatiasArriola committed Jan 27, 2025
1 parent 0362149 commit 54dc76d
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
rules: {
"no-console": ["warn", { allow: ["debug", "warn", "error", "info"] }],
"no-console": ["warn", { allow: ["debug", "warn", "error", "info", "table"] }],
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/explicit-function-return-type": ["off"],
"@typescript-eslint/no-this-alias": ["off"],
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,64 @@ shell:~$ yarn start trackedEntities transfer \
--input-file=transfers.csv \
--post
```

## SMS

### Gateways

#### List SMS Gateways

```console
shell:~$ yarn start gateways list \
--url="http://admin:district@localhost:8080"
```

#### View a SMS Gateway config

View detailed configuration from a specific gateway.

```console
shell:~$ yarn start sms gateways info \
--url="http://admin:district@localhost:8080" \
--uid="HW4X1IYOtK"
```

#### Save a SMS Gateway config to a config file

```console
shell:~$ yarn -s start sms gateways info \
--url="http://admin:district@localhost:8080" \
--uid="HW4X1IYOtK" > custom-http-gateway.json
```

#### Create SMS Gateway from config file

```console
shell:~$ yarn start sms gateways create \
--url="http://admin:district@localhost:8080"
--config-file=custom-http-gateway.json
```

### Delete a SMS Gateway

```console
shell:~$ yarn start sms gateways delete \
--url="http://admin:district@localhost:8080" \
--uid="HW4X1IYOtK"
```

### Send SMS

```console
shell:~$ yarn start sms send \
--url="http://admin:district@localhost:8080"
--to="123123123"
--message="This is a test message from d2-tools"
```

### List latest Outbound SMSs

```console
shell:~$ yarn start sms outbound \
--url="http://admin:district@localhost:8080"
```
72 changes: 72 additions & 0 deletions src/data/SmsD2Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { D2Api } from "types/d2-api";
import { Async } from "domain/entities/Async";
import { SmsGateway } from "domain/entities/SmsGateway";
import { SendSmsOptions, SmsOperationResponse, SmsRepository } from "domain/repositories/SmsRepository";
import { Id } from "domain/entities/Base";
import { SmsOutbound } from "domain/entities/SmsOutbound";
import { Pager } from "domain/entities/Pager";

export interface D2SmsGatewayParameter {
header: boolean;
encode: boolean;
key: string;
value: string;
confidential: boolean;
}

export class SmsD2Repository implements SmsRepository {
constructor(private api: D2Api) {}

async getGateways(): Async<SmsGateway[]> {
const { gateways } = await this.api.get<{ gateways: SmsGateway[] }>("gateways").getData();
return gateways;
}

async getGatewayById(uid: Id): Async<SmsGateway> {
const gateway = await this.api.get<SmsGateway>(`gateways/${uid}`).getData();
return gateway;
}

async createGateway(gatewayInfo: SmsGateway): Async<SmsOperationResponse> {
const response = await this.api.post<SmsOperationResponse>("gateways", {}, gatewayInfo).getData();
return response;
}

async updateGateway(uid: Id, gatewayInfo: SmsGateway): Async<SmsOperationResponse> {
const response = await this.api
.put<SmsOperationResponse>(`gateways/${uid}`, {}, gatewayInfo)
.getData();

return response;
}

async deleteGateway(uid: Id): Async<SmsOperationResponse> {
const response = await this.api.delete<SmsOperationResponse>(`gateways/${uid}`).getData();

return response;
}

async sendSMS(payload: SendSmsOptions): Async<SmsOperationResponse> {
const response = await this.api
.post<SmsOperationResponse>(
"sms/outbound",
{},
{
message: payload.message,
recipients: [payload.recipient],
}
)
.getData();
return response;
}

async getOutboundSmsList(): Async<SmsOutbound[]> {
const response = await this.api
.get<{ pager: Pager; outboundsmss: SmsOutbound[] }>("sms/outbound", {
fields: "*",
order: "date:desc",
})
.getData();
return response.outboundsmss;
}
}
46 changes: 46 additions & 0 deletions src/domain/entities/SmsGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { D2SmsGatewayParameter } from "data/SmsD2Repository";
import { Id } from "./Base";

export type SmsGateway = ClickatellGateway | BulkSmsGateway | SMPPGateway | GenericHttpSmsGateway;

interface SmsGatewayBase {
uid?: Id;
name: string;
isDefault: boolean;
}

export interface ClickatellGateway extends SmsGatewayBase {
type: "clickatell";
username: string;
authToken: string;
urlTemplate: string;
}

export interface BulkSmsGateway extends SmsGatewayBase {
type: "bulksms";
username: string;
password: string;
}

export interface SMPPGateway extends SmsGatewayBase {
type: "smpp";
systemId: string;
host: string;
systemType: string;
numberPlanIndicator: string;
typeOfNumber: string;
bindType: string;
port: number;
password: string;
compressed: boolean;
}

export interface GenericHttpSmsGateway extends SmsGatewayBase {
type: "http";
configurationTemplate: string;
useGet: boolean;
sendUrlParameters: boolean;
urlTemplate: string;
contentType: string;
parameters: Array<D2SmsGatewayParameter>;
}
9 changes: 9 additions & 0 deletions src/domain/entities/SmsOutbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Id } from "./Base";

export interface SmsOutbound {
id: Id;
status: string;
recipients: string[];
message: string;
date: string;
}
25 changes: 25 additions & 0 deletions src/domain/repositories/SmsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Async } from "domain/entities/Async";
import { Id } from "domain/entities/Base";
import { SmsGateway } from "domain/entities/SmsGateway";
import { SmsOutbound } from "domain/entities/SmsOutbound";

export interface SendSmsOptions {
message: string;
recipient: string;
}

export interface SmsOperationResponse {
status: "OK" | "ERROR";
message: string;
}

export interface SmsRepository {
getGateways(): Async<SmsGateway[]>;
getGatewayById(uid: Id): Async<SmsGateway>;
createGateway(gatewayInfo: SmsGateway): Async<SmsOperationResponse>;
updateGateway(uid: Id, gatewayInfo: SmsGateway): Async<SmsOperationResponse>;
deleteGateway(uid: Id): Async<SmsOperationResponse>;

sendSMS(payload: SendSmsOptions): Async<SmsOperationResponse>;
getOutboundSmsList(): Async<SmsOutbound[]>;
}
17 changes: 17 additions & 0 deletions src/domain/usecases/sms/CreateSmsGatewayFromConfigUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Async } from "domain/entities/Async";
import { SmsRepository } from "domain/repositories/SmsRepository";
import fs from "fs";
import path from "path";
import _ from "lodash";
import { SmsGateway } from "domain/entities/SmsGateway";

export class CreateSmsGatewayFromConfigUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(configFilePath: string): Async<void> {
const config = JSON.parse(fs.readFileSync(path.join(".", configFilePath), "utf8"));
const newGateway = _.omit(config, ["uid"]) as SmsGateway;
const result = await this.smsRepository.createGateway(newGateway);
console.info(`${result.status}: ${result.message}`);
}
}
13 changes: 13 additions & 0 deletions src/domain/usecases/sms/DeleteSmsGatewayUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Async } from "domain/entities/Async";
import { Id } from "domain/entities/Base";
import { SmsRepository } from "domain/repositories/SmsRepository";
import _ from "lodash";

export class DeleteSmsGatewayUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(id: Id): Async<void> {
const result = await this.smsRepository.deleteGateway(id);
console.info(`${result.status}: ${result.message}`);
}
}
16 changes: 16 additions & 0 deletions src/domain/usecases/sms/SendSmsUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Async } from "domain/entities/Async";
import { SmsRepository } from "domain/repositories/SmsRepository";

export class SendSmsUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(options: SendSmsUseCaseOptions): Async<void> {
const result = await this.smsRepository.sendSMS(options);
console.info(`${result.status}: ${result.message}`);
}
}

export interface SendSmsUseCaseOptions {
recipient: string;
message: string;
}
12 changes: 12 additions & 0 deletions src/domain/usecases/sms/ShowSmsGatewayInfoUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Async } from "domain/entities/Async";
import { Id } from "domain/entities/Base";
import { SmsRepository } from "domain/repositories/SmsRepository";

export class ShowSmsGatewayInfoUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(id: Id): Async<void> {
const gateway = await this.smsRepository.getGatewayById(id);
console.info(JSON.stringify(gateway, null, 4));
}
}
15 changes: 15 additions & 0 deletions src/domain/usecases/sms/ShowSmsGatewayListUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Async } from "domain/entities/Async";
import { SmsRepository } from "domain/repositories/SmsRepository";

export class ShowSmsGatewayListUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(): Async<void> {
const gateways = await this.smsRepository.getGateways();
if (gateways.length === 0) {
console.info("No gateways found");
return;
}
console.table(gateways, ["uid", "isDefault", "type", "name", "urlTemplate"]);
}
}
17 changes: 17 additions & 0 deletions src/domain/usecases/sms/ShowSmsOutboundListUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Async } from "domain/entities/Async";
import { SmsRepository } from "domain/repositories/SmsRepository";

export class ShowSmsOutboundListUseCase {
constructor(private smsRepository: SmsRepository) {}

async execute(): Async<void> {
const outboundMessages = await this.smsRepository.getOutboundSmsList();
console.table(
outboundMessages.map(m => ({
...m,
recipients: m.recipients.join(", "),
})),
["date", "status", "recipients", "message"]
);
}
}
2 changes: 2 additions & 0 deletions src/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as categoryoptions from "./commands/categoryoptions";
import * as indicators from "./commands/indicators";
import * as trackedEntities from "./commands/trackedEntities";
import * as enrollments from "./commands/enrollments";
import * as sms from "./commands/sms";

export function runCli() {
const cliSubcommands = subcommands({
Expand All @@ -36,6 +37,7 @@ export function runCli() {
categoryoptions: categoryoptions.getCommand(),
indicators: indicators.getCommand(),
enrollments: enrollments.getCommand(),
sms: sms.getCommand(),
},
});

Expand Down
Loading

0 comments on commit 54dc76d

Please sign in to comment.