src/index.js
import AWS from 'aws-sdk';
import csv from 'fast-csv';
const PADDING_BASE_NUM = 10;
/**
* Record
*/
class Record {
/**
* constructor
*
* @param {Object} row row of CSV
*/
constructor(row) {
this.isPayer = !row.LinkedAccountId;
this.productCode = row.ProductCode;
this.totalCost = Number.parseFloat(row.TotalCost);
this.isTotal = Array.includes(['AccountTotal', 'StatementTotal'], row.RecordType);
this.isService = Array.includes(['PayerLineItem', 'LinkedLineItem'], row.RecordType);
this.accountId = row.LinkedAccountId || row.PayerAccountId;
this.accountName = row.LinkedAccountName || row.PayerAccountName;
}
}
/**
* Bill
*/
class Bill {
/**
* constructor
*
* @param {Record} record initial data
*/
constructor(record) {
this.accountId = record.accountId;
this.accountName = record.accountName;
this.isPayer = record.isPayer;
this.totalCost = 0;
this.products = {};
}
get sortedProducts() {
return Object.keys(this.products).map(code => {
return {
code,
shortCode: code.replace(/aws|amazon/i, ''),
cost: this.products[code]
};
}).sort((a, b) => b.cost - a.cost);
}
/**
* update
*
* @param {Record} record update self
*/
update(record) {
if (record.isTotal) {
this.totalCost = record.totalCost;
} else if (record.isService) {
const code = record.productCode;
if (this.products[code] === undefined) {
this.products[code] = 0;
}
this.products[code] += record.totalCost;
}
}
}
/**
* Report
*/
class Report {
/**
* constructor
*/
constructor() {
this.bills = {};
}
/**
* Billing of total
*
* @return {Bill}
*/
get total() {
return this.bills.total;
}
/**
* accounts
*
* @return {Bill[]}
*/
get accounts() {
return Object.keys(this.bills).filter(k => k !== 'total').map(k => {
return this.bills[k];
}).sort((a, b) => b.totalCost - a.totalCost);
}
/**
* update
*
* @param {Record} record update self
*/
update(record) {
const id = record.isPayer ? 'total' : record.accountId;
if (!this.bills[id]) {
this.bills[id] = new Bill(record);
}
this.bills[id].update(record);
}
}
/**
* zero padding
*
* @param {number} number target number
* @return {string}
*/
function pad(number) {
if (number < PADDING_BASE_NUM) {
return `0${number}`;
}
return number.toString();
}
/**
* Kanjo
*/
export default class Kanjo {
/**
* constructor
*
* @param {string} account account id
* @param {string} bucket bucket name
* @param {string|undefined} region region
* @param {string|undefined} accessKeyId AWS Access key ID
* @param {string|undefined} secretAccessKey AWS Secret Access key
*/
constructor({account, bucket, region, accessKeyId, secretAccessKey}) {
this.account = account;
this.bucket = bucket;
this.region = region;
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
}
/**
* fetch billing data
*
* @param {number} year target year
* @param {number} month target month
* @return {Promise}
*/
fetch(year, month) {
const opts = {region: this.region};
if (this.accessKeyId) {
opts.accessKeyId = this.accessKeyId;
}
if (this.secretAccessKey) {
opts.secretAccessKey = this.secretAccessKey;
}
const s3 = new AWS.S3(opts);
const key = `${this.account}-aws-billing-csv-${year}-${pad(month)}.csv`;
return new Promise((resolve, reject) => {
s3.getObject({
Bucket: this.bucket,
Key: key
}, (err, data) => {
if (err) {
reject(err);
} else {
const report = new Report();
csv.fromString(data.Body.toString('utf8'), {headers: true})
.on('data', row => {
report.update(new Record(row));
})
.on('end', () => resolve(report))
.on('error', _err => reject(_err));
}
});
});
}
}