-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmain.go
207 lines (174 loc) · 6.5 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package main
// BUG(spacesailor24) calculateHours method does not match total time calculation done by Clockify.
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math"
"net/http"
"os"
"regexp"
"strconv"
"strings"
)
// configObj used to populate PDF with non-standard values.
type configObj struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
InvoiceNumber float64 `json:"invoiceNumber"`
RatePerHour float64 `json:"ratePerHour"`
Name string `json:"name"`
Email string `json:"email"`
Address string `json:"address"`
InvoicePeriod string `json:"invoicePeriod"`
Notes string `json:"notes"`
InvoiceDataFilePath string `json:"invoiceDataFilePath"`
OutPutFilePath string `json:"outputFilePath"`
}
// clockifyEntry needed values from Clockify's response data to construct invoiceEntry.
type clockifyEntry struct {
Description string `json:"description"`
ProjectName string `json:"projectName"`
ClientName string `json:"clientName"`
Duration string `json:"duration"`
}
// invoiceEntry needed values to construct Clockify time entries.
type invoiceEntry struct {
Name string `json:"name"`
Quantity float64 `json:"quantity"`
UnitCost float64 `json:"unit_cost"`
}
// invoiceRequestData data being sent to InvoiceGeneratorURL to generate PDF.
type invoiceRequestData struct {
From string `json:"from"`
To string `json:"to"`
Number float64 `json:"number"`
Items []invoiceEntry `json:"items"`
Notes string `json:"notes"`
}
// invoiceGeneratorURL URL invoiceRequestData is sent to, to generate PDF.
const invoiceGeneratorURL = "https://invoice-generator.com"
var config configObj
var clockifyEntries []clockifyEntry
func main() {
configPathPtr := flag.String("c", "config.json", "Config file for personal invoice details")
flag.Parse()
parseConfigFile(*configPathPtr)
parseDataFile(config.InvoiceDataFilePath)
var invoiceEntries []invoiceEntry
for i := 0; i < len(clockifyEntries); i++ {
invoiceEntries = append(invoiceEntries, buildInvoiceEntry(clockifyEntries[i]))
}
generateInvoice(buildRequestData(invoiceEntries), config.OutPutFilePath)
}
// check if error is found, panics.
func check(e error) {
if e != nil {
panic(e)
}
}
// parseConfigFile reads filePath and unmarshals into config type object.
func parseConfigFile(filePath string) {
bytesFile := getFileAsBytes(filePath)
json.Unmarshal(bytesFile, &config)
}
// parseDataFile reads filePath and unmarshals into clockifyEntries type object.
func parseDataFile(filePath string) {
bytesFile := getFileAsBytes(filePath)
json.Unmarshal(bytesFile, &clockifyEntries)
}
// getFileAsBytes reads filePath and returns bytes value.
func getFileAsBytes(filePath string) []byte {
jsonFile, err := os.Open(filePath)
check(err)
defer jsonFile.Close()
byteValue, err := ioutil.ReadAll(jsonFile)
check(err)
return byteValue
}
// buildInvoiceEntry uses provided data to generate a single invoiceEntry that will be displayed in generated invoice.
func buildInvoiceEntry(entryData clockifyEntry) invoiceEntry {
invoiceEntryName := buildInvoiceEntryName(entryData.ClientName, entryData.ProjectName, entryData.Description)
invoiceQuantity := getInvoiceEntryQuantity(entryData.Duration)
return invoiceEntry{Name: invoiceEntryName, Quantity: invoiceQuantity, UnitCost: config.RatePerHour}
}
// buildInvoiceEntryName returns formatted string used to represent what was worked on.
// The format of the string is generic and can be changed to anything.
func buildInvoiceEntryName(clientName string, projectName string, description string) string {
return fmt.Sprintf("Client Name: %s | Project Name: %s | Description: %s", clientName, projectName, description)
}
// getInvoiceEntryQuantity parses a Clockify formatted duration string (in the format of PT1H1M1S) into
// each individual time segment.
// Returns total amount of hours worked.
func getInvoiceEntryQuantity(duration string) float64 {
var hours float64
var minutes float64
var seconds float64
re := regexp.MustCompile(`PT(\d+H)?(\d+M)?(\d+S)?`)
for _, match := range re.FindStringSubmatch(duration) {
if strings.HasPrefix(match, "P") {
continue
} else if strings.HasSuffix(match, "H") {
hours, _ = strconv.ParseFloat(strings.TrimSuffix(match, "H"), 64)
} else if strings.HasSuffix(match, "M") {
minutes, _ = strconv.ParseFloat(strings.TrimSuffix(match, "M"), 64)
} else if strings.HasSuffix(match, "S") {
seconds, _ = strconv.ParseFloat(strings.TrimSuffix(match, "S"), 64)
}
}
return calculateHours(hours, minutes, seconds)
}
// calculateHours uses provided data to calculate the amount of hours worked.
// Rounded to nearest 1/10th of an hour.
func calculateHours(numHours float64, numMinutes float64, numSeconds float64) float64 {
if numSeconds > 0 {
numMinutes++
}
return numHours + toFixed(numMinutes/60, 1)
}
// round Helper function to round float64s.
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}
// toFixed Rounds a number to given number of decimals.
func toFixed(num float64, precision int) float64 {
output := math.Pow(10, float64(precision))
return float64(round(num*output)) / output
}
// buildRequestData Builds need JSON object to submit to invoice generater.
func buildRequestData(invoiceEntries []invoiceEntry) []byte {
requestData := invoiceRequestData{From: config.Sender, To: config.Receiver, Number: config.InvoiceNumber, Items: invoiceEntries, Notes: buildNotes()}
bytes, err := json.Marshal(requestData)
check(err)
return bytes
}
// buildNotes Uses configObj to build notes section of invoice.
func buildNotes() string {
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", config.Name, config.Email, config.Address, config.InvoicePeriod, config.Notes)
}
// generateInvoice Takes requestData and submits data to invoiceGeneratorURL and save PDF to outputFileName.
func generateInvoice(requestData []byte, outputFileName string) {
req, err := http.NewRequest("POST", invoiceGeneratorURL, bytes.NewBuffer(requestData))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
check(err)
defer resp.Body.Close()
if resp.Status == "200 OK" {
body, _ := ioutil.ReadAll(resp.Body)
createFile(body, outputFileName)
}
}
// createFile Write data to filePath.
func createFile(data []byte, filePath string) {
f, err := os.Create(filePath)
defer f.Close()
check(err)
f.Write(data)
fmt.Println("Invoice generated successfully")
}