Skip to content

Commit

Permalink
テスト改善&リファクタリング (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
gentksb authored Mar 2, 2024
1 parent 6bc0308 commit 8b00f4b
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 252 deletions.
158 changes: 32 additions & 126 deletions lambda/spilitwise-automation/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,143 +4,49 @@ import {
APIGatewayEvent,
Handler,
} from "aws-lambda";
import axios, { AxiosRequestConfig } from "axios";
import { splitExpense } from "./src/logic/splitExpense";
import { isExpenseEligibleForSplitting } from "./src/validator/isExpenseEligibleForSplitting";
import { paths } from "../../@types/splitwise";

// I tried to generate d.type.ts, w/ official API repo and openapi-typescript, but it didn't work.
// https://github.com/drwpow/openapi-typescript
// https://github.com/splitwise/api-docs
import { splitRecent20Expenses } from "./main";

export const handler: Handler = async (
event: APIGatewayEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
const { SPLITWISE_API_KEY_PARAMETER_NAME, SLACK_WEBHOOK_URL } = process.env;
const axios_option: AxiosRequestConfig = {
headers: { Authorization: `Bearer ${SPLITWISE_API_KEY_PARAMETER_NAME}` },
};
const { USER1_ID, USER2_ID, SPLITWISE_GROUP_ID } = process.env;

if (SLACK_WEBHOOK_URL === undefined) {
throw new Error("slack url is not set");
}

axios.interceptors.response.use(
(response) => response,
async (error) => {
console.error(error);
return;
}
);

// 本処理
const getExpenses = await axios.get(
"https://secure.splitwise.com/api/v3.0/get_expenses",
axios_option
);

const expensesList: paths["/get_expenses"]["get"]["responses"]["200"]["content"]["application/json"]["expenses"] =
getExpenses.data.expenses;

// リストが空か0の場合は処理を終了
if (expensesList === undefined || expensesList.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({
message: "取得対象の精算経費がありません",
}),
};
const {
SPLITWISE_API_KEY_PARAMETER_NAME,
SLACK_WEBHOOK_URL,
USER1_ID,
USER1_RATE,
USER2_ID,
USER2_RATE,
SPLITWISE_GROUP_ID,
} = process.env;

// validation of environment variables
if (
SPLITWISE_API_KEY_PARAMETER_NAME === undefined ||
USER1_ID === undefined ||
USER1_RATE === undefined ||
USER2_ID === undefined ||
USER2_RATE === undefined ||
SPLITWISE_GROUP_ID === undefined ||
SLACK_WEBHOOK_URL === undefined
) {
throw new Error("環境変数が設定されていません");
}

const willSplitExpenses = expensesList.filter((expense) =>
isExpenseEligibleForSplitting(expense)
);

// 更新処理
await Promise.all(
willSplitExpenses.map(async (expense) => {
console.log("更新処理開始 ExpenseID: ", expense.id);
const payerId = expense.repayments?.[0]?.to?.toString();
const { payerOwedShare, nonPayerOwedShare } = splitExpense(expense);

const newSplitData = {
users__0__user_id: payerId,
users__0__paid_share: expense.cost,
users__0__owed_share: payerOwedShare,
users__1__user_id: payerId === USER1_ID ? USER2_ID : USER1_ID,
users__1__paid_share: "0",
users__1__owed_share: nonPayerOwedShare.toString(),
};

await axios
.post(
`https://secure.splitwise.com/api/v3.0/update_expense/${expense.id}`,
{
group_id: SPLITWISE_GROUP_ID,
...newSplitData,
},
{
headers: {
"Content-Type": "application/json",
...axios_option.headers,
},
}
)
.then(async (response) => {
if (Object.keys(response.data.errors).length !== 0) {
console.error(response.data.errors);
await axios.post(SLACK_WEBHOOK_URL, {
text: `割り勘処理でエラー発生\n ID:${response.data.expenses[0].id}\n${response.data.errors.shares}\n${response.data.errors.base}`,
});
throw new Error("Splitwise APIへのPOST内容に問題があります");
} else {
console.log(response.data);

const slackMessage = [
`ID:${response.data.expenses[0].id} を下記の通り分割しました`,
`\`\`\`●内容: ${response.data.expenses[0].description}`,
`●金額: ${response.data.expenses[0].cost}円`,
`●${response.data.expenses[0].users[0].user.first_name}の負担: ${response.data.expenses[0].users[0].owed_share}円`,
`●${response.data.expenses[0].users[1].user.first_name}の負担: ${response.data.expenses[0].users[1].owed_share}円\`\`\``,
].join("\n");

await axios.post(SLACK_WEBHOOK_URL, {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "割り勘補正を実行しました:ballot_box_with_check:",
emoji: true,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: slackMessage,
},
},
],
});
}
});
return;
})
);

const logMessage =
expensesList.length === 0
? "取得対象の精算経費がありません"
: `直近${expensesList.length}の経費のうち、${willSplitExpenses.length}件を割り勘処理しました`;
console.log(logMessage);
splitRecent20Expenses({
SPLITWISE_API_KEY_PARAMETER_NAME,
SLACK_WEBHOOK_URL,
USER1_ID,
USER1_RATE,
USER2_ID,
USER2_RATE,
SPLITWISE_GROUP_ID,
});

return {
statusCode: 200,
body: JSON.stringify({
message: logMessage,
message: "処理が完了しました",
}),
};
};
170 changes: 170 additions & 0 deletions lambda/spilitwise-automation/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { splitExpense } from "./src/logic/splitExpense";
import { isExpenseEligibleForSplitting } from "./src/validator/isExpenseEligibleForSplitting";
import { components, paths } from "../../@types/splitwise";

interface Props {
SPLITWISE_API_KEY_PARAMETER_NAME: string;
SLACK_WEBHOOK_URL: string;
USER1_ID: string;
USER1_RATE: string;
USER2_ID: string;
USER2_RATE: string;
SPLITWISE_GROUP_ID: string;
}

type Expense = components["schemas"]["expense"];

export const splitRecent20Expenses = async (props: Props) => {
const {
SPLITWISE_API_KEY_PARAMETER_NAME,
SLACK_WEBHOOK_URL,
USER1_ID,
USER1_RATE,
USER2_ID,
USER2_RATE,
SPLITWISE_GROUP_ID,
} = props;

const axios_option: AxiosRequestConfig = {
headers: { Authorization: `Bearer ${SPLITWISE_API_KEY_PARAMETER_NAME}` },
};

axios.interceptors.response.use(
(response) => response,
async (error) => {
console.error(error);
return;
}
);

// 本処理
const getExpenses: AxiosResponse<
paths["/get_expenses"]["get"]["responses"]["200"]["content"]["application/json"]
> = await axios.get(
"https://secure.splitwise.com/api/v3.0/get_expenses",
axios_option
);

const expensesList: Expense[] = getExpenses.data.expenses || [];

// リストが空か0の場合は処理を終了
if (expensesList.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({
message: "取得対象の精算経費がありません",
}),
};
}

const willSplitExpenses = expensesList.filter((expense) =>
isExpenseEligibleForSplitting({
expense,
USER1_RATE,
USER2_RATE,
SPLITWISE_GROUP_ID,
})
);

const makeNewSplitData = (expense: Expense) => {
const payerId = expense.repayments?.[0]?.to?.toString();
const { payerOwedShare, nonPayerOwedShare } = splitExpense({
expense,
USER1_RATE,
USER1_ID,
USER2_RATE,
});

return {
group_id: SPLITWISE_GROUP_ID,
users__0__user_id: payerId,
users__0__paid_share: expense.cost,
users__0__owed_share: payerOwedShare,
users__1__user_id: payerId === USER1_ID ? USER2_ID : USER1_ID,
users__1__paid_share: "0",
users__1__owed_share: nonPayerOwedShare.toString(),
};
};

// 更新処理
await Promise.all(
willSplitExpenses.map(async (expense) => {
console.log("更新処理開始 ExpenseID: ", expense.id);
const newSplitData = makeNewSplitData(expense);

await axios
.post(
`https://secure.splitwise.com/api/v3.0/update_expense/${expense.id}`,
newSplitData,
{
headers: {
"Content-Type": "application/json",
...axios_option.headers,
},
}
)
.then(
async (
response: AxiosResponse<
paths["/update_expense/{id}"]["post"]["responses"]["200"]["content"]["application/json"]
>
) => {
if (response.data.errors?.length !== 0) {
console.error(response.data.errors);
await axios.post(SLACK_WEBHOOK_URL, {
text: `割り勘処理でエラー発生\n ID:${response.data.expenses?.[0].id}\n${response.data.errors?.toString()}`,
});
throw new Error("Splitwise APIへのPOST内容に問題があります");
} else {
console.log("update_expense成功", response.data);

const slackMessage = [
`ID:${response.data.expenses?.[0].id} を下記の通り分割しました`,
`\`\`\`●内容: ${response.data.expenses?.[0].description}`,
`●金額: ${response.data.expenses?.[0].cost}円`,
`●${response.data.expenses?.[0].users?.[0].user?.first_name}の負担: ${response.data.expenses?.[0].users?.[0].owed_share}円`,
`●${response.data.expenses?.[0].users?.[1].user?.first_name}の負担: ${response.data.expenses?.[0].users?.[1].owed_share}円\`\`\``,
].join("\n");

await axios
.post(SLACK_WEBHOOK_URL, {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "割り勘補正を実行しました:ballot_box_with_check:",
emoji: true,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: slackMessage,
},
},
],
})
.then((res) =>
console.log(
`Slack post response: ${res.status}`,
res.data.toString()
)
);
}
}
);
return;
})
);

const logMessage =
expensesList.length === 0
? "取得対象の精算経費がありません"
: `直近${expensesList.length}の経費のうち、${willSplitExpenses.length}件を割り勘処理しました`;
console.log(logMessage);

return { result: logMessage };
};
16 changes: 14 additions & 2 deletions lambda/spilitwise-automation/src/logic/splitExpense.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { components } from "../../../../@types/splitwise";

export const splitExpense = (expense: components["schemas"]["expense"]) => {
const { USER1_RATE, USER2_RATE, USER1_ID } = process.env;
type Expense = components["schemas"]["expense"];

interface Props {
expense: Expense;
USER1_RATE: string;
USER1_ID: string;
USER2_RATE: string;
}

export const splitExpense = ({
expense,
USER1_RATE,
USER1_ID,
USER2_RATE,
}: Props) => {
// ユーザー情報が環境変数に入力されているかどうかのチェック
if (USER1_RATE != null && USER2_RATE != null && USER1_ID !== null) {
const numCost = parseInt(expense.cost ?? "0");
Expand Down
Loading

0 comments on commit 8b00f4b

Please sign in to comment.