Skip to content

Commit eefa2f5

Browse files
committed
Fixed usages and billing page
1 parent 66f2b6a commit eefa2f5

7 files changed

Lines changed: 286 additions & 1 deletion

File tree

api/pkg/entities/billing_usage.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type BillingUsage struct {
1212
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1313
SentMessages uint `json:"sent_messages" example:"321"`
1414
ReceivedMessages uint `json:"received_messages" example:"465"`
15+
TotalCost uint `json:"total_cost" example:"0"`
1516
StartTimestamp time.Time `json:"start_timestamp" example:"2022-01-01T00:00:00+00:00"`
1617
EndTimestamp time.Time `json:"end_timestamp" example:"2022-01-31T23:59:59+00:00"`
1718
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`

api/pkg/repositories/gorm_billing_usage_repository.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,17 @@ func (repository *gormBillingUsageRepository) GetCurrent(ctx context.Context, us
8787

8888
err := crdbgorm.ExecuteTx(ctx, repository.db, nil,
8989
func(tx *gorm.DB) error {
90+
loadedUsage := &entities.BillingUsage{}
9091
result := tx.WithContext(ctx).
92+
Where("user_id = ?", userID).
9193
Where("start_timestamp = ?", now.New(timestamp).BeginningOfMonth()).
92-
First(usage)
94+
First(&loadedUsage)
9395

9496
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
9597
return tx.WithContext(ctx).Create(usage).Error
9698
}
99+
100+
*usage = *loadedUsage
97101
return result.Error
98102
},
99103
)

web/components/MessageThreadHeader.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@
137137
</v-list-item-title>
138138
</v-list-item-content>
139139
</v-list-item>
140+
<v-list-item :to="{ name: 'billing' }" exact>
141+
<v-list-item-icon class="pl-2">
142+
<v-icon dense>{{ mdiFinance }}</v-icon>
143+
</v-list-item-icon>
144+
<v-list-item-content class="ml-n3">
145+
<v-list-item-title class="pr-16 py-1">
146+
<span :class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }">
147+
Usage & Billing
148+
</span>
149+
</v-list-item-title>
150+
</v-list-item-content>
151+
</v-list-item>
140152
<v-list-item @click.prevent="logout">
141153
<v-list-item-icon class="pl-2">
142154
<v-icon dense>{{ mdiLogout }}</v-icon>
@@ -162,6 +174,7 @@ import {
162174
mdiAccountCog,
163175
mdiLogout,
164176
mdiDownload,
177+
mdiFinance,
165178
mdiPackageUp,
166179
mdiPackageDown,
167180
mdiDotsVertical,
@@ -178,6 +191,7 @@ export default class MessageThreadHeader extends Vue {
178191
mdiLogout = mdiLogout
179192
mdiDownload = mdiDownload
180193
mdiPackageUp = mdiPackageUp
194+
mdiFinance = mdiFinance
181195
mdiPackageDown = mdiPackageDown
182196
mdiDotsVertical = mdiDotsVertical
183197
mdiCircle = mdiCircle

web/models/billing.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface BillingUsage {
2+
id: string
3+
start_timestamp: string
4+
end_timestamp: string
5+
user_id: string
6+
sent_messages: number
7+
received_messages: number
8+
total_cost: number
9+
created_at: string
10+
}

web/pages/billing/index.vue

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<template>
2+
<v-container fluid class="pa-0" :fill-height="$vuetify.breakpoint.lgAndUp">
3+
<div class="w-full h-full">
4+
<v-app-bar height="60" :dense="$vuetify.breakpoint.mdAndDown">
5+
<v-btn icon to="/">
6+
<v-icon>{{ mdiArrowLeft }}</v-icon>
7+
</v-btn>
8+
<v-toolbar-title>
9+
<div class="py-16">Account Usage</div>
10+
</v-toolbar-title>
11+
<v-progress-linear
12+
:active="loading"
13+
:indeterminate="loading"
14+
absolute
15+
bottom
16+
></v-progress-linear>
17+
</v-app-bar>
18+
<v-container>
19+
<v-row>
20+
<v-col cols="12" md="9" offset-md="1" xl="8" offset-xl="2">
21+
<h5 class="text-h4 mb-3 mt-3">Overview</h5>
22+
<p class="text--secondary">
23+
This is the summary of the sent messages and received messages in
24+
<code
25+
v-if="$store.getters.getBillingUsage"
26+
class="font-weight-bold"
27+
>{{
28+
$store.getters.getBillingUsage.start_timestamp | billingPeriod
29+
}}</code
30+
>.
31+
</p>
32+
<v-row v-if="$store.getters.getBillingUsage">
33+
<v-col cols="12" md="4">
34+
<v-alert
35+
dark
36+
dense
37+
:icon="mdiCallMade"
38+
prominent
39+
type="info"
40+
text
41+
>
42+
<h2 class="text-h4 font-weight-bold mt-4">
43+
{{ $store.getters.getBillingUsage.sent_messages | decimal }}
44+
</h2>
45+
<p class="text--secondary mt-n1">Messages Sent</p>
46+
</v-alert>
47+
</v-col>
48+
<v-col cols="12" md="4">
49+
<v-alert
50+
dark
51+
dense
52+
:icon="mdiCallReceived"
53+
prominent
54+
type="warning"
55+
text
56+
>
57+
<div class="d-flex">
58+
<h2 class="text-h4 font-weight-bold mt-4">
59+
{{
60+
$store.getters.getBillingUsage.received_messages
61+
| decimal
62+
}}
63+
</h2>
64+
</div>
65+
<p class="text--secondary mt-n1">Messages Received</p>
66+
</v-alert>
67+
</v-col>
68+
<v-col cols="12" md="4">
69+
<v-alert
70+
dense
71+
:icon="mdiCreditCard"
72+
prominent
73+
type="success"
74+
text
75+
>
76+
<h2 class="text-h4 font-weight-bold mt-4">
77+
{{ $store.getters.getBillingUsage.total_cost | money }}
78+
</h2>
79+
<p class="text--secondary mt-n1">Total Cost</p>
80+
</v-alert>
81+
</v-col>
82+
</v-row>
83+
<h5 class="text-h4 mb-3 mt-12">Usage History</h5>
84+
<p class="text--secondary">
85+
Summary of all the sent and received messages in the past 12
86+
months
87+
</p>
88+
<v-simple-table>
89+
<template #default>
90+
<thead>
91+
<tr class="text-uppercase">
92+
<th class="text-left">Period</th>
93+
<th class="text-left">
94+
Sent
95+
<span v-if="$vuetify.breakpoint.lgAndUp">Messages</span>
96+
</th>
97+
<th class="text-left">
98+
Received
99+
<span v-if="$vuetify.breakpoint.lgAndUp">Messages</span>
100+
</th>
101+
<th class="text-right">
102+
<span v-if="$vuetify.breakpoint.lgAndUp">Total</span> Cost
103+
</th>
104+
</tr>
105+
</thead>
106+
<tbody>
107+
<tr
108+
v-for="billingUsage in $store.getters
109+
.getBillingUsageHistory"
110+
:key="billingUsage.id"
111+
>
112+
<td>
113+
{{ billingUsage.start_timestamp | billingPeriod }}
114+
</td>
115+
<td>
116+
{{ billingUsage.sent_messages | decimal }}
117+
</td>
118+
<td>
119+
{{ billingUsage.received_messages }}
120+
</td>
121+
<td class="text-right font-weight-bold">
122+
{{ billingUsage.total_cost | money }}
123+
</td>
124+
</tr>
125+
</tbody>
126+
</template>
127+
</v-simple-table>
128+
</v-col>
129+
</v-row>
130+
</v-container>
131+
</div>
132+
</v-container>
133+
</template>
134+
135+
<script lang="ts">
136+
import Vue from 'vue'
137+
import {
138+
mdiArrowLeft,
139+
mdiAccountCircle,
140+
mdiShieldCheck,
141+
mdiDelete,
142+
mdiContentSave,
143+
mdiEye,
144+
mdiEyeOff,
145+
mdiCallReceived,
146+
mdiCallMade,
147+
mdiCreditCard,
148+
mdiSquareEditOutline,
149+
} from '@mdi/js'
150+
151+
export default Vue.extend({
152+
name: 'BillingIndex',
153+
middleware: ['auth'],
154+
data() {
155+
return {
156+
mdiEye,
157+
mdiEyeOff,
158+
mdiArrowLeft,
159+
mdiAccountCircle,
160+
mdiShieldCheck,
161+
mdiDelete,
162+
mdiContentSave,
163+
mdiCallReceived,
164+
mdiCallMade,
165+
mdiCreditCard,
166+
mdiSquareEditOutline,
167+
loading: true,
168+
}
169+
},
170+
head() {
171+
return {
172+
title: 'Usage & Billing - Http SMS',
173+
}
174+
},
175+
computed: {
176+
apiKey() {
177+
if (this.$store.getters.getUser === null) {
178+
return ''
179+
}
180+
return this.$store.getters.getUser.api_key
181+
},
182+
},
183+
async mounted() {
184+
if (!this.$store.getters.getAuthUser) {
185+
await this.$store.dispatch('setNextRoute', this.$route.path)
186+
await this.$router.push({ name: 'index' })
187+
setTimeout(this.loadData, 2000)
188+
return
189+
}
190+
await this.loadData()
191+
},
192+
193+
methods: {
194+
async loadData() {
195+
await Promise.all([
196+
this.$store.dispatch('loadBillingUsage'),
197+
this.$store.dispatch('loadBillingUsageHistory'),
198+
])
199+
this.loading = false
200+
},
201+
},
202+
})
203+
</script>

web/plugins/filters.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,25 @@ Vue.filter('phoneCountry', (value: string): string => {
2525
Vue.filter('timestamp', (value: string): string => {
2626
return new Date(value).toLocaleString()
2727
})
28+
29+
Vue.filter('money', (value: string): string => {
30+
return new Intl.NumberFormat('en-US', {
31+
style: 'currency',
32+
currency: 'USD',
33+
}).format(parseInt(value))
34+
})
35+
36+
Vue.filter('decimal', (value: string): string => {
37+
return new Intl.NumberFormat('en-US', {
38+
style: 'decimal',
39+
}).format(parseInt(value))
40+
})
41+
42+
Vue.filter('billingPeriod', (value: string): string => {
43+
const options = {
44+
year: 'numeric',
45+
month: 'long',
46+
}
47+
// @ts-ignore
48+
return new Date(value).toLocaleDateString('en-US', options)
49+
})

web/store/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Heartbeat } from '~/models/heartbeat'
66
import axios from '~/plugins/axios'
77
import { Phone } from '~/models/phone'
88
import { User } from '~/models/user'
9+
import { BillingUsage } from '~/models/billing'
910

1011
const defaultNotificationTimeout = 3000
1112

@@ -35,6 +36,8 @@ export type State = {
3536
loadingMessages: boolean
3637
archivedThreads: boolean
3738
authUser: AuthUser | null
39+
billingUsage: BillingUsage | null
40+
billingUsageHistory: Array<BillingUsage>
3841
user: User | null
3942
phones: Array<Phone>
4043
threads: Array<MessageThread>
@@ -52,6 +55,8 @@ export const state = (): State => ({
5255
nextRoute: null,
5356
axiosError: null,
5457
loadingThreads: true,
58+
billingUsage: null,
59+
billingUsageHistory: [],
5560
archivedThreads: false,
5661
loadingMessages: true,
5762
pooling: false,
@@ -117,6 +122,14 @@ export const getters = {
117122
return state.user
118123
},
119124

125+
getBillingUsageHistory(state: State): Array<BillingUsage> {
126+
return state.billingUsageHistory
127+
},
128+
129+
getBillingUsage(state: State): BillingUsage | null {
130+
return state.billingUsage
131+
},
132+
120133
getOwner(state: State): string | null {
121134
return state.owner
122135
},
@@ -186,6 +199,12 @@ export const mutations = {
186199
setThreadId(state: State, payload: string | null) {
187200
state.threadId = payload
188201
},
202+
setBillingUsageHistory(state: State, payload: Array<BillingUsage>) {
203+
state.billingUsageHistory = payload
204+
},
205+
setBillingUsage(state: State, payload: BillingUsage | null) {
206+
state.billingUsage = payload
207+
},
189208
setThreadMessages(state: State, payload: Array<Message>) {
190209
state.threadMessages = payload
191210
state.loadingMessages = false
@@ -242,6 +261,8 @@ export const mutations = {
242261

243262
resetState(state: State) {
244263
state.threads = []
264+
state.billingUsage = null
265+
state.billingUsageHistory = []
245266
state.phones = []
246267
state.user = null
247268
state.threadId = null
@@ -282,6 +303,16 @@ export const actions = {
282303
await context.commit('setThreads', response.data.data)
283304
},
284305

306+
async loadBillingUsage(context: ActionContext<State, State>) {
307+
const response = await axios.get('/v1/billing/usage')
308+
await context.commit('setBillingUsage', response.data.data)
309+
},
310+
311+
async loadBillingUsageHistory(context: ActionContext<State, State>) {
312+
const response = await axios.get('/v1/billing/usage-history')
313+
await context.commit('setBillingUsageHistory', response.data.data)
314+
},
315+
285316
async toggleArchive(context: ActionContext<State, State>) {
286317
await context.commit('setArchivedThreads', !context.getters.getIsArchived)
287318
},

0 commit comments

Comments
 (0)