INITIAL COMMIT
0
.dockerignore
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
k8s/deployment.yml
|
||||||
|
bin/
|
||||||
|
dolardb/
|
||||||
|
|
||||||
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM debian:unstable-slim
|
||||||
|
ARG BINAME=us-dop-api-linux-arm64-0.0.0_1
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY ./assets/ /app/assets/
|
||||||
|
|
||||||
|
COPY ./bin/${BINAME} /usr/bin/us-dop-api
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["us-dop-api"]
|
||||||
61
Makefile
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# must create a .env file with info
|
||||||
|
# must have compose installed
|
||||||
|
include .env
|
||||||
|
export
|
||||||
|
OS:=${shell go env GOOS}
|
||||||
|
ARCH=$(shell go env GOARCH)
|
||||||
|
OOSS="linux"
|
||||||
|
ARRCHS="arm 386"
|
||||||
|
DEBUG=1
|
||||||
|
SERVICE=us-dop-api
|
||||||
|
VERSION=0.0.0_1
|
||||||
|
BINAME=$(SERVICE)-$(OS)-$(ARCH)-$(VERSION)
|
||||||
|
BINAMEARM=$(SERVICE)-$(OS)-arm64-$(VERSION)
|
||||||
|
# can be docker or podman or whatever
|
||||||
|
CONTAINERS=docker
|
||||||
|
COMPOSE=$(CONTAINERS)-compose
|
||||||
|
# Configure local registry
|
||||||
|
REGADDR=192.168.0.151:32000
|
||||||
|
K8SRSNAME=$(shell kubectl get rs --no-headers -o custom-columns=":metadata.name" | grep us-dop-api)
|
||||||
|
.phony: all clean build test clean-image build-image build-image-debug run-image run-image-debug run-local
|
||||||
|
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
# here we made the images and push to registry with buildx
|
||||||
|
@$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/$(SERVICE):latest .
|
||||||
|
# Here we upload it to local
|
||||||
|
|
||||||
|
run-image: build-image
|
||||||
|
@$(CONTAINERS) compose -f docker-compose.yaml up
|
||||||
|
|
||||||
|
build-image-debug: clean
|
||||||
|
@$(CONTAINERS) compose -f docker-compose-debug.yaml build
|
||||||
|
|
||||||
|
run-image-debug: build-image-debug
|
||||||
|
@$(CONTAINERS) compose -f docker-compose-debug.yaml up
|
||||||
|
|
||||||
|
run-local:clean build
|
||||||
|
@bin/$(BINAME)
|
||||||
|
|
||||||
|
build:
|
||||||
|
@mkdir dolardb || true
|
||||||
|
@env GOOS=$(OS) GOARCH=$(arch) go build -o ./bin/$(BINAME) ./cmd/api/.
|
||||||
|
@env GOOS=$(OS) GOARCH=arm64 go build -o ./bin/$(BINAMEARM) ./cmd/api/.
|
||||||
|
|
||||||
|
create-descriptors:
|
||||||
|
@envsubst < k8s/deployment.yml.template > k8s/deployment.yml
|
||||||
|
|
||||||
|
deploy: build-image create-descriptors
|
||||||
|
@kubectl apply -f k8s/deployment.yml
|
||||||
|
@kubectl scale rs $(K8SRSNAME) --replicas=0
|
||||||
|
@kubectl scale rs $(K8SRSNAME) --replicas=1
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go -count=1 test ./...
|
||||||
|
clean:
|
||||||
|
@rm -rf ./bin
|
||||||
|
|
||||||
|
clean-image:
|
||||||
|
@$(CONTAINERS) system prune -f
|
||||||
|
|
||||||
|
|
||||||
124
adapters/dolar/dolar.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package dolar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maximotejeda/msvc-proto/golang/dolar"
|
||||||
|
"github.com/maximotejeda/us_dop_api/domain"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
dolar dolar.DollarClient
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(conn *grpc.ClientConn) (*Adapter, error) {
|
||||||
|
client := dolar.NewDollarClient(conn)
|
||||||
|
return &Adapter{dolar: client, conn: conn}, nil
|
||||||
|
}
|
||||||
|
func (a *Adapter) GetLatest(name string) (*domain.History, error) {
|
||||||
|
hr, err := a.dolar.GetLatest(context.Background(), &dolar.GetLatestRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
date := time.Unix(hr.Actual.Parsed, 0)
|
||||||
|
loc, _ := time.LoadLocation("America/Santo_Domingo")
|
||||||
|
history := &domain.History{
|
||||||
|
ID: hr.Actual.Id,
|
||||||
|
Institution: domain.Institution{
|
||||||
|
Name: hr.Actual.Name,
|
||||||
|
},
|
||||||
|
Compra: float64(hr.Actual.Compra),
|
||||||
|
Venta: float64(hr.Actual.Venta),
|
||||||
|
Parser: hr.Actual.Parser,
|
||||||
|
Parsed: date.In(loc).Format(time.DateTime),
|
||||||
|
}
|
||||||
|
return history, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Adapter) GetSince(name string, duration int64) (list []*domain.History, err error) {
|
||||||
|
hrl, err := a.dolar.GetSince(context.Background(), &dolar.GetSinceRequest{
|
||||||
|
Name: name,
|
||||||
|
Duration: duration,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list = []*domain.History{}
|
||||||
|
|
||||||
|
for _, hr := range hrl.Histories {
|
||||||
|
date := time.Unix(hr.Parsed, 0)
|
||||||
|
loc, _ := time.LoadLocation("America/Santo_Domingo")
|
||||||
|
hist := &domain.History{
|
||||||
|
ID: hr.Id,
|
||||||
|
Institution: domain.Institution{
|
||||||
|
Name: hr.Name,
|
||||||
|
},
|
||||||
|
Compra: float64(hr.Compra),
|
||||||
|
Venta: float64(hr.Venta),
|
||||||
|
Parser: hr.Parser,
|
||||||
|
Parsed: date.In(loc).Format(time.DateTime),
|
||||||
|
}
|
||||||
|
list = append(list, hist)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Adapter) GetInstByType(name string) (list []string, err error) {
|
||||||
|
hrl, err := a.dolar.GetInstByType(context.Background(), &dolar.GetInstByTypeRequest{
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list = []string{}
|
||||||
|
|
||||||
|
list = append(list, hrl.InstList...)
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Adapter) Subscribe(tgbid int64, instName string) (bool, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err := a.dolar.TGBSubscribe(ctx, &dolar.TGBSubscribeRequest{TgbId: tgbid, InstName: instName})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Adapter) Unsubscribe(tgbid int64, instName string) (bool, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err := a.dolar.TGBUnsubscribe(ctx, &dolar.TGBUnsubscribeRequest{TgbId: tgbid, InstName: instName})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
func (a Adapter) GetSubscribedUsers(instName string) ([]int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
users, err := a.dolar.TGBGetSubscribedUsers(ctx, &dolar.TGBGetSubscribedUsersRequest{InstName: instName})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list := []int64{}
|
||||||
|
list = append(list, users.TgbIds...)
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
func (a Adapter) GetSubscribedInsts(tgbid int64) ([]string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
insts, err := a.dolar.TGBGetSubscribedInsts(ctx, &dolar.TGBGetSubscribedInstRequest{TgbId: tgbid})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list := []string{}
|
||||||
|
|
||||||
|
list = append(list, insts.InstName...)
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
124
assets/css/comparer.css
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
.hidden{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compare-page{
|
||||||
|
#toogleinst{
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
align-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
color: var(--root-color, white);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--root-border-color, white);
|
||||||
|
background-color: var(--root-bg-color-secondary, hsla(250, 100%, 3%, 1));
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 1px solid var(--root-border-color, white);
|
||||||
|
background-color: var(--root-bg-color-secondary, black);
|
||||||
|
align-self: center;
|
||||||
|
color: var(--root-color, white);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#response-institution {
|
||||||
|
|
||||||
|
#inst-result-list-container {
|
||||||
|
|
||||||
|
#inst-result-list{
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.card{
|
||||||
|
.inst-link{
|
||||||
|
width: 86px;
|
||||||
|
height: 33px;
|
||||||
|
img{
|
||||||
|
width: 78px;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end inst-result-list */
|
||||||
|
} /* end inst-result-list-container */
|
||||||
|
div{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
div#control-compra-venta {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
div#control-cantidad{
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
justify-content: center;
|
||||||
|
input{
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--root-border-color, white);
|
||||||
|
background-color: var(--root-bg-color-secondary, hsla(250, 50%, 20%, 0.3));
|
||||||
|
color: var(--root-color, white);
|
||||||
|
font-size: large;
|
||||||
|
width: 10ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div#operation-description{
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#show-price-container{
|
||||||
|
justify-content: center;
|
||||||
|
div#container-comparer-table{
|
||||||
|
justify-content: center;
|
||||||
|
img{
|
||||||
|
width: 78px;
|
||||||
|
height: 28px;
|
||||||
|
background-color: var( --root-bg-color-img, white);
|
||||||
|
|
||||||
|
}
|
||||||
|
table{
|
||||||
|
thead th:nth-child(1) {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
thead th:nth-child(2) {
|
||||||
|
width: 15%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
thead th:nth-child(3) {
|
||||||
|
width: 15%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
thead th:nth-child(4) {
|
||||||
|
width: 10%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
thead th:nth-child(5) {
|
||||||
|
width: 30%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
/* body content */
|
||||||
|
tbody tr td:nth-child(1),tbody tr td:nth-child(2), tbody tr td:nth-child(3) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
tbody tr td:nth-child(4){
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
tbody tr td:nth-child(5){
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end response-institution */
|
||||||
|
} /* compare-page end */
|
||||||
291
assets/css/main.css
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
/* hello */
|
||||||
|
:root{
|
||||||
|
--root-bg-color: black;
|
||||||
|
--root-bg-color-secondary: hsla(250, 50%, 20%, 0.2);
|
||||||
|
--root-bg-color-secondary-tewwo: hsla(250, 50%, 95%, 0.2);
|
||||||
|
--root-color: white;
|
||||||
|
--root-border-color: white;
|
||||||
|
--root-bg-color-btn: rgba(50, 50, 50, 0.5);
|
||||||
|
--root-bg-color-btn-hoover: rgb(50 50 50);
|
||||||
|
--root-bg-color-btn-focus: rgb(50 50 50);
|
||||||
|
--root-bg-color-img: white;
|
||||||
|
--root-shadow-color-btn-opened: rgba(0, 250, 0, 0.5);
|
||||||
|
--root-shadow-color-btn-cloded: rgba(250, 0, 0, 0.5);
|
||||||
|
--root-shadow-color-img: rgb(50 100 50);
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: hsla(100, 50%, 0%, 0.1);
|
||||||
|
border-radius: 17px;
|
||||||
|
border: 1px solid gray;
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle:after{
|
||||||
|
content: '';
|
||||||
|
text-align: center;
|
||||||
|
text-justify: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: gray;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
.checkbox:checked + .toggle::after {
|
||||||
|
left : 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:checked + .toggle {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display : none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
background-color: var(--root-bg-color, black);
|
||||||
|
color: var(--root-color, white);
|
||||||
|
|
||||||
|
#app {
|
||||||
|
#root{
|
||||||
|
p {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#root-controls{
|
||||||
|
|
||||||
|
#toogle-root-controls{
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
align-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
color: var(--root-color, white);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-secondary, black);
|
||||||
|
|
||||||
|
#toogle-root-contols-btn{
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin-right: 10px;
|
||||||
|
align-self: center;
|
||||||
|
border: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-btn, black);
|
||||||
|
color: var(--root-color, white);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
#theme-container{
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: 10px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#root-select-control{
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
button{
|
||||||
|
align-self: center;
|
||||||
|
border: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-btn, black);
|
||||||
|
color: var(--root-color, white);;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#inst-selector {
|
||||||
|
#toogle-hist-control {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
align-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
color: var(--root-color, white);;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-secondary, black);
|
||||||
|
|
||||||
|
button{
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-btn, black);
|
||||||
|
align-self: center;
|
||||||
|
color: var(--root-color, white);;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#inst-type-selector{
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 35px;
|
||||||
|
button{
|
||||||
|
background-color: var(--root-bg-color-btn, black);
|
||||||
|
/* color: #fff; */
|
||||||
|
/* border: none; */
|
||||||
|
/* border-radius: 10px; */
|
||||||
|
/* box-shadow: 0px 1px 3px 2px rgb(90 90 90); */
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 70px;
|
||||||
|
border: 1px solid var(--root-border-color, white);;
|
||||||
|
background-color: var(--root-bg-color-btn, black);
|
||||||
|
align-self: center;
|
||||||
|
color: var(--root-color, white);;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
} /* <- end #inst-type-selector */
|
||||||
|
#response-div{
|
||||||
|
height: 70;
|
||||||
|
#inst-result-list-container{
|
||||||
|
#inst-result-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
list-style-type: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
.card {
|
||||||
|
.inst-link{
|
||||||
|
width: 86px;
|
||||||
|
height: 33px;
|
||||||
|
img{
|
||||||
|
width: 78px;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end #response-div*/
|
||||||
|
#response-inst-card{
|
||||||
|
display: flex;
|
||||||
|
width: 370px;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
#inst-selected{
|
||||||
|
|
||||||
|
}
|
||||||
|
form{
|
||||||
|
width: 360px;
|
||||||
|
border-radius: 6px;
|
||||||
|
#inst-selected{
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
img {
|
||||||
|
/* originally the dimensions where width: 155px, height: 55px */
|
||||||
|
background-color: var( --root-bg-color-img, white);;
|
||||||
|
padding: 2px;
|
||||||
|
width: 93px;
|
||||||
|
height: 33px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0px 0px 3px 4px var(--root-shadow-color-img, rgb(50 100 50));
|
||||||
|
}
|
||||||
|
} /* inst-selected */
|
||||||
|
fieldset{
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-width: 0.5px;
|
||||||
|
box-shadow: 0px 2px 3px 2px rgb(150 100 100);
|
||||||
|
border-width: 0px;
|
||||||
|
#tf-container{
|
||||||
|
margin: 10px 0px;
|
||||||
|
background: linear-gradient(to bottom, rgb( 70 70 70 / 60%), rgb(150 150 150 / 30%));
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
} /* end fieldset */
|
||||||
|
label#time-amount-selector{
|
||||||
|
width: 93px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
}
|
||||||
|
input#button-submit{
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#response-inst-table-container{
|
||||||
|
display: flex;
|
||||||
|
table{
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 2px solid var(--root-border-color, white);;
|
||||||
|
thead th:nth-child(1) {
|
||||||
|
width: 19%;
|
||||||
|
}
|
||||||
|
thead th:nth-child(2) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
thead th:nth-child(3) {
|
||||||
|
width: 19%;
|
||||||
|
}
|
||||||
|
thead th:nth-child(4) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
thead th:nth-child(5) {
|
||||||
|
width: 32%;
|
||||||
|
}
|
||||||
|
td, th{
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
|
span.green{
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
span.red{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
thead,
|
||||||
|
tfoot {
|
||||||
|
/* background: url(leopardskin.jpg); */
|
||||||
|
color: var(--root-color, white);;
|
||||||
|
text-shadow: 1px 1px 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th, tfoot th, tfoot td {
|
||||||
|
background: linear-gradient(to bottom, rgb( 0 0 70 / 10%), rgb(100 100 100 / 60%));
|
||||||
|
border: 3px solid hsla(270, 100%, 20%, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opened{
|
||||||
|
box-shadow: 0px 2px 2px 2px var(--root-shadow-color-btn-opened, green);
|
||||||
|
}
|
||||||
|
.closed{
|
||||||
|
box-shadow: 0px 1px 1px 1px var(--root-shadow-color-btn-closed, red);
|
||||||
|
}
|
||||||
0
assets/html/index.html
Normal file
12
assets/html/other.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><p>this is the app</p></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
assets/img/a-cibao.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets/img/a-la-nacional.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/img/a-peravia.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/img/a-popular.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/img/acn.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/img/banco-activo-dominicana.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/img/banco-ademi.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/img/banco-atlantico.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/img/banco-bdi.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/img/banco-caribe.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/img/banco-central.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/img/banco-hipotecario.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/img/banco-lafise.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/banco-lopez-de-haro.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/img/banco-popular.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/img/banco-promerica.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/img/banco-santa-cruz.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/img/banco-vimenca.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/img/banesco-icon.png
Normal file
|
After Width: | Height: | Size: 934 B |
BIN
assets/img/banesco.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/img/banreservas.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/img/bonanza-banco.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/img/cambio-extranjero.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/capla.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/img/gamelin.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/img/girosol.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/img/imbert-y-balbuena.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/img/moneycorps.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/img/motor-credito.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/img/rm.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/scotiabank.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/sct.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
assets/img/taveras.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
5
assets/js/alpine.min.js
vendored
Normal file
65
assets/js/htmx.min.js
vendored
Normal file
163
assets/js/main.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
|
||||||
|
function enableSelector(e){
|
||||||
|
const value = document.querySelector("#ta-value");
|
||||||
|
const input = document.querySelector("#ta-amount");
|
||||||
|
const instSelectors = document.querySelector("#response-inst-card");
|
||||||
|
const instNameSelector = document.querySelector("#inst-name-selector");
|
||||||
|
|
||||||
|
|
||||||
|
value.textContent = input.value;
|
||||||
|
input.addEventListener("input", (event) =>{
|
||||||
|
value.textContent = event.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const name = document.querySelector("#inst-name-selector");
|
||||||
|
console.log(e.target.name);
|
||||||
|
name.value = e.target.name;
|
||||||
|
instSelectors.hidden=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', ()=>{
|
||||||
|
Alpine.data('app', ()=>({
|
||||||
|
open: false,
|
||||||
|
instSelected: '',
|
||||||
|
instSelectedImage: '',
|
||||||
|
tf: '',
|
||||||
|
ta: '',
|
||||||
|
compvalue: 1,
|
||||||
|
compoperation: 'compra',
|
||||||
|
buttoninst: {},
|
||||||
|
theme: 'dark',
|
||||||
|
get urler() { return '/api/instituciones/history';},
|
||||||
|
// print representation of search history on form
|
||||||
|
get queryRepr() {
|
||||||
|
if (this.instSelected === ""|| this.tf === ""){
|
||||||
|
console.log(this.instSelected, this.tf);
|
||||||
|
return "Seleciona institucion y periodo de tiempo";
|
||||||
|
}
|
||||||
|
const tc = document.querySelector("#response-inst-table-container");
|
||||||
|
let tfstr = '';
|
||||||
|
switch (this.tf){
|
||||||
|
case 'week':
|
||||||
|
tfstr = 'semanas';
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
tfstr = 'dias';
|
||||||
|
break;
|
||||||
|
case 'hour':
|
||||||
|
tfstr = 'horas';
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
tfstr = 'meses';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return `Busqueda: desde hace ${this.ta} ${tfstr} en ${this.instSelected}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
// return table to empty content con history
|
||||||
|
resetTableC(){
|
||||||
|
const tc = document.querySelector("#response-inst-table-container");
|
||||||
|
tc.innerHTML = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
// change div visibility on comparer
|
||||||
|
toglediv(e){
|
||||||
|
// lets change button content
|
||||||
|
let clase = "id-" + e.target.name;
|
||||||
|
const container = document.querySelector(`#${clase}`);
|
||||||
|
console.log(clase);
|
||||||
|
|
||||||
|
if (container.classList.contains("hidden")) {
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
}else {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// calculate value on a table depending on options provided
|
||||||
|
calcvalue(e){
|
||||||
|
const quantity = e.target.value;
|
||||||
|
const operation = document.getElementById("comparar-operacion");
|
||||||
|
const containers = document.getElementsByClassName("comparer-row");
|
||||||
|
if ( containers.length === 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let cont of containers){
|
||||||
|
const compra = document.getElementById(cont.id+"-compra");
|
||||||
|
const venta = document.getElementById(cont.id+"-venta");
|
||||||
|
const result = document.getElementById(cont.id+"-res");
|
||||||
|
const currency = document.getElementById(cont.id+"-cur");
|
||||||
|
if (isNaN(this.compvalue)|| this.compvalue == 0){
|
||||||
|
console.log("is not a number", this.compvalue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (this.compoperation){
|
||||||
|
case "compra":
|
||||||
|
result.innerHTML = Number((this.compvalue / venta.innerHTML).toFixed(2)).toLocaleString();
|
||||||
|
currency.innerHTML = "$ US";
|
||||||
|
break;
|
||||||
|
case "venta":
|
||||||
|
result.innerHTML = Number((compra.innerHTML * this.compvalue).toFixed(2)).toLocaleString();
|
||||||
|
currency.innerHTML = "$ RD";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// comparemos si compra y venta son numeros
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get reprCompOperation(){
|
||||||
|
switch (this.compoperation){
|
||||||
|
case "compra":
|
||||||
|
return `Comprando dolares con ${Number(this.compvalue).toFixed(2).toLocaleString()} $RD pesos.`;
|
||||||
|
break;
|
||||||
|
case "venta":
|
||||||
|
return `Vendiendo ${Number(this.compvalue).toFixed(2).toLocaleString()} dolares con cambios a $RD pesos.`;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
themeChange(e){
|
||||||
|
|
||||||
|
let r = document.documentElement.style;
|
||||||
|
|
||||||
|
switch (e.target.checked){
|
||||||
|
case false:
|
||||||
|
r.setProperty('--root-bg-color', 'black');
|
||||||
|
r.setProperty("--root-bg-color-secondary", "hsla(250, 50%, 20%, 0.2);");
|
||||||
|
r.setProperty("--root-border-color", "white");
|
||||||
|
r.setProperty("--root-color", "white");
|
||||||
|
r.setProperty("--root-bg-color-btn", "rgba(50, 50, 50, 0.5)");
|
||||||
|
r.setProperty("--root-bg-color-btn-hoover", "rgb(50 50 50)");
|
||||||
|
r.setProperty("--root-bg-color-btn-focus", "rgb(50 50 50)");
|
||||||
|
r.setProperty("--root-bg-color-img", "white");
|
||||||
|
r.setProperty("--root-shadow-color-btn-opened", "rgba(0, 250, 0, 0.5)");
|
||||||
|
r.setProperty("--root-shadow-color-btn-cloded", "rgba(250, 0, 0, 0.5)");
|
||||||
|
r.setProperty("--root-shadow-color-img", "rgb(50 100 50)");
|
||||||
|
console.log(this.theme);
|
||||||
|
break;
|
||||||
|
case true:
|
||||||
|
r.setProperty('--root-bg-color', 'white');
|
||||||
|
r.setProperty("--root-bg-color-secondary", "hsla(250, 50%, 85%, 0.2)");
|
||||||
|
r.setProperty("--root-color", "black");
|
||||||
|
r.setProperty("--root-border-color", "black");
|
||||||
|
r.setProperty("--root-bg-color-btn", "rgba(80, 80, 80, 0.2)");
|
||||||
|
r.setProperty("--root-bg-color-btn-hoover", "rgb(50 50 50)");
|
||||||
|
r.setProperty("--root-bg-color-btn-focus", "rgb(50 50 50)");
|
||||||
|
r.setProperty("--root-bg-color-img", "white");
|
||||||
|
r.setProperty("--root-shadow-color-btn-opened", "rgba(0, 250, 0, 0.5)");
|
||||||
|
r.setProperty("--root-shadow-color-btn-cloded", "rgba(250, 0, 0, 0.5)");
|
||||||
|
r.setProperty("--root-shadow-color-img", "rgb(50 100 50)");
|
||||||
|
console.log(this.theme);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:afterSwap",(e)=>{
|
||||||
|
const page = document.querySelector("#page");
|
||||||
|
|
||||||
|
htmx.process(e.target);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
0
assets/templates/components/header.html.tmpl
Normal file
76
assets/templates/components/institution.html.tmpl
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{{ define "card" }}
|
||||||
|
{{ if ne .InstRequested "" }}
|
||||||
|
{{ $inst := .InstRequested }}
|
||||||
|
{{ range .Results }}
|
||||||
|
<div class="card">
|
||||||
|
<p class="inst-name">
|
||||||
|
{{ .Name }}
|
||||||
|
<span class="institucion">{{ $inst }}</span>
|
||||||
|
</p>
|
||||||
|
<a class="inst-link" href="/api/instituciones/{{ .Institution.Name }}/history?ta=4&tf=week&format=html">{{ .Institution.Name }}</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- definicion de lista -->
|
||||||
|
{{ define "list" }}
|
||||||
|
{{ if ne .InstRequested "" }}
|
||||||
|
{{ $inst := .InstRequested }}
|
||||||
|
<div id="inst-result-list-container" {{if eq $inst "all"}} x-show="comparerpage" {{ end }}>
|
||||||
|
<ul id="inst-result-list">
|
||||||
|
{{ range $idx, $obj := .Results }}
|
||||||
|
{{$idname := .Institution.Name | replace " " "-" }}
|
||||||
|
<li class="card">
|
||||||
|
{{ $name := .Institution.Name | replace " dominicano" "" | replace "asociacion " "a "| replace " de ahorros y prestamos" ""| replace " cambio online" "" | replace " " "-" }}
|
||||||
|
{{ $idname := .Institution.Name| replace " " "-" }}
|
||||||
|
<button
|
||||||
|
class="inst-link btn-{{ $idx }}"
|
||||||
|
name="{{ $idname }}"
|
||||||
|
style='{{ if eq $idname "banco-vimenca" }} background-color:#1789E1 {{ end }}'
|
||||||
|
{{ if ne $inst "all" }}
|
||||||
|
href="/api/instituciones/{{ $obj.Institution.Name }}/history?ta=4&tf=week&format=html"
|
||||||
|
x-bind:class='instSelected === "{{ $obj.Institution.Name }}" ? "opened" : ""'
|
||||||
|
@click='
|
||||||
|
open = true;
|
||||||
|
instSelected = "{{ $obj.Institution.Name }}";
|
||||||
|
instSelectedImage = "/static/img/{{ $name }}.png";
|
||||||
|
resetTableC();
|
||||||
|
'
|
||||||
|
{{ else }}
|
||||||
|
x-init="buttoninst['{{ $idname }}'] = false;"
|
||||||
|
@click='toglediv; buttoninst["{{ $idname }}"] = ! buttoninst["{{ $idname }}"];'
|
||||||
|
x-bind:class="buttoninst['{{ $idname }}'] ? 'opened' : '' "
|
||||||
|
href="/"
|
||||||
|
hx-target="id-{{ .Institution.Name }}"
|
||||||
|
{{ end }}
|
||||||
|
>
|
||||||
|
<img class="img-{{ $idx }}" name="{{ $idname }}" src='/static/img/{{ $name }}.png' alt="bank logo" >
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ if eq $inst "all" }}
|
||||||
|
<div>
|
||||||
|
<div id="control-compra-venta">
|
||||||
|
<label>Compra</label>
|
||||||
|
<input type="radio" value="compra" @change="calcvalue" x-on:input="compoperation = 'compra'" checked name="operation">
|
||||||
|
<label>Venta</label>
|
||||||
|
<input type="radio" value="venta" @change="calcvalue" x-on:input="compoperation = 'venta'" name="operation">
|
||||||
|
</div>
|
||||||
|
<div id="control-cantidad">
|
||||||
|
<label>Cantidad</label>
|
||||||
|
<input @change="calcvalue" x-model="compvalue" x-on:input="calcvalue" inputmode="numeric" maxlength="7" pattern="[0-9.]{1,7}">
|
||||||
|
</div>
|
||||||
|
<div id="operation-description">
|
||||||
|
<p x-text="reprCompOperation"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="show-price-container" hx-get="/api/latest/all" hx-trigger="load delay:1sec">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
124
assets/templates/components/institution_history.html.tmpl
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
{{ define "card-history" }}
|
||||||
|
{{ if ne .InstRequested "" }}
|
||||||
|
{{ $inst := .InstRequested }}
|
||||||
|
{{ range .Results }}
|
||||||
|
<div class="card">
|
||||||
|
<p class="inst-name">
|
||||||
|
{{ .Institution.Name }}
|
||||||
|
<span class="institucion">{{ $inst }}</span>
|
||||||
|
</p>
|
||||||
|
<a class="inst-link" href="/api/instituciones/{{ .Institution.Name }}/history?ta=4&tf=week&format=html">{{ .Institution.Name }}</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- definicion de lista -->
|
||||||
|
{{ define "list-history" }}
|
||||||
|
{{ if ne .InstRequested "" }}
|
||||||
|
{{ $inst := .InstRequested }}
|
||||||
|
<div>
|
||||||
|
<p>{{ $inst }}</p>
|
||||||
|
<ul>
|
||||||
|
{{ range $idx, $obj := .Results }}
|
||||||
|
<li class="lista item-{{ $idx }}">
|
||||||
|
<p class="inst-name">
|
||||||
|
<span class="compra inst list">Compra: RD. {{ $obj.Compra }} </span>
|
||||||
|
<span class="venta inst list">Venta: RD. {{ $obj.Venta }} </span>
|
||||||
|
<span class="time inst list">Fecha: {{ $obj.Parsed }} </span>
|
||||||
|
<span class="parser inst list">Parser: {{ $obj.Parser }} </span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- crate table -->
|
||||||
|
{{ define "table-history" }}
|
||||||
|
{{ if ne .InstRequested "" }}
|
||||||
|
<table class="inst {{ .InstRequested }}" name="{{ .InstRequested }}">
|
||||||
|
<caption>Historico de precios para {{ .InstRequested }} los precios se encuentran en la moneda local de Republica Dominicana RD.</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!-- <th>Nombre</th> -->
|
||||||
|
<th>Compra</th>
|
||||||
|
<th>+/-</th>
|
||||||
|
<th>Venta</th>
|
||||||
|
<th>+/-</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th class="table-parser" hidden="true">Parser</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ $acumComp := 0.0 }}
|
||||||
|
{{ $acumVent := 0.0 }}
|
||||||
|
{{ $actualComp := 0.0 }}
|
||||||
|
{{ $actualVent := 0.0 }}
|
||||||
|
{{ $antComp := 0.0 }}
|
||||||
|
{{ $antVent := 0.0 }}
|
||||||
|
{{ range $idx, $obj := .Results }}
|
||||||
|
<tr class="row-{{ $idx }}">
|
||||||
|
<td>
|
||||||
|
{{ if eq $idx 0}}
|
||||||
|
|
||||||
|
{{ $actualComp = $obj.Compra }}
|
||||||
|
{{ $actualVent = $obj.Venta }}
|
||||||
|
{{ end }}
|
||||||
|
<span id="row-{{ $idx }}-compra" class="compra amount">{{ $obj.Compra | printf "%0.2f"}}</span>
|
||||||
|
</td>
|
||||||
|
<td >
|
||||||
|
{{ if ne $antComp 0.0 }}
|
||||||
|
{{ $tmpVarCompra := mult $obj.Compra -1.0 | add $antComp}}
|
||||||
|
<span class="compra diff {{if gt $tmpVarCompra 0.0 }} green {{ else if lt $tmpVarCompra 0.0 }} red {{end}}">{{ $tmpVarCompra | printf "%10.2f" }} </span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span id="row-{{ $idx }}-venta" class="venta amount">{{ $obj.Venta | printf "%0.2f" }}</span></td>
|
||||||
|
<td >
|
||||||
|
{{ if ne $antVent 0.0 }}
|
||||||
|
{{ $tmpVarVenta := mult $obj.Venta -1.0 | add $antVent }}
|
||||||
|
<span class="venta diff {{if gt $tmpVarVenta 0.0 }} green {{ else if lt $tmpVarVenta 0.0 }} red {{end}}">{{ $tmpVarVenta | printf "%10.2f" }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td> <span class="fecha">{{ $obj.Parsed }}</span></td>
|
||||||
|
<td class="table-parser" hidden="true">{{ $obj.Parser }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ $acumComp = add $acumComp $obj.Compra }}
|
||||||
|
{{ $acumVent = add $acumVent $obj.Venta }}
|
||||||
|
{{ $antComp = $obj.Compra }}
|
||||||
|
{{ $antVent = $obj.Venta }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if le (len .Results) 0 }}
|
||||||
|
<tr>
|
||||||
|
<th>Error</th><td>Sin historico en este periodo</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<!-- Calculo de la acumulacion de los distintos valores media y diferencia -->
|
||||||
|
<!-- solo muestra footer si el nunero de celdas es mayor a 1 -->
|
||||||
|
{{if gt (len .Results) 1 }}
|
||||||
|
<tr>
|
||||||
|
<th>C Media</th>
|
||||||
|
{{ $mediaComp := len .Results | div $acumComp}}
|
||||||
|
{{ $diffMediaCompra:= mult $actualComp -1.0 | add $mediaComp | mult -1.0 }}
|
||||||
|
<td><span class="compra media">{{ $mediaComp | printf "%.2f" }}</span></td>
|
||||||
|
<th>V Media</th>
|
||||||
|
{{ $mediaVent := len .Results | div $acumVent }}
|
||||||
|
{{ $diffMediaVent := mult $actualVent -1.0 | add $mediaVent | mult -1.0}}
|
||||||
|
<td><span class="venta media">{{ $mediaVent | printf "%.2f" }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th id="inst-table-dcompra-header">C Diff </th>
|
||||||
|
<td id="inst-table-dcompra-data"><span class="compra media diff {{if gt $diffMediaCompra 0.0 }} green {{ else }}red{{end}}"> {{ $diffMediaCompra | printf "%0.3f" }}</span></td>
|
||||||
|
|
||||||
|
<th id="inst-table-dventa-header">V Diff</th>
|
||||||
|
<td id="inst-table-dventa-data"> <span class="venta media diff {{if gt $diffMediaVent 0.0 }} green {{ else }}red{{end}}">{{ $diffMediaVent | printf "%0.3f" }}</span></td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
{{ end}}
|
||||||
|
{{ end }}
|
||||||
37
assets/templates/components/latestprice.html.tmpl
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{{ define "latest-price" }}
|
||||||
|
|
||||||
|
<div id="container-comparer-table" class="comparer-container">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th> Institucion</th>
|
||||||
|
<th>Compra</th>
|
||||||
|
<th>Venta</th>
|
||||||
|
<th>Moneda</th>
|
||||||
|
<th> Aprox </th>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range . }}
|
||||||
|
{{ $idname := .Institution.Name | replace " " "-"}}
|
||||||
|
{{ $name := .Institution.Name | replace " dominicano" "" | replace "asociacion " "a "| replace " de ahorros y prestamos" ""| replace " cambio online" "" | replace " " "-" }}
|
||||||
|
<tr id="id-{{ $idname }}" class="comparer-row hidden">
|
||||||
|
<td><img class="compare-img img-{{ $idname }}" name="{{ $idname }}" src='/static/img/{{ $name }}.png' alt="bank logo" > </td>
|
||||||
|
<!-- <label>{{ .Institution.Name }}</label> -->
|
||||||
|
<td id="id-{{ $idname }}-compra" class="comparer compra">{{ .Compra | printf "%.2f" }}</td>
|
||||||
|
<td id="id-{{ $idname }}-venta" class="comparer venta">{{ .Venta | printf "%.2f" }}</td>
|
||||||
|
<td id="id-{{ $idname }}-cur" class="comparer currency"></td>
|
||||||
|
<td id="id-{{ $idname }}-res">0.00</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "all-latest-price" }}
|
||||||
|
<!-- {{ range $idx, $obj := . }} -->
|
||||||
|
{{ template "latest-price" . }}
|
||||||
|
<!-- {{ end }} -->
|
||||||
|
{{ end }}
|
||||||
81
assets/templates/index.html.tmpl
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{{ define "index" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<link rel="stylesheet" href="/static/css/main.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/comparer.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
||||||
|
<!-- <script src="/static/js/htmx.min.js"></script> -->
|
||||||
|
<!-- <script defer src="/static/js/alpine.min.js" ></script> -->
|
||||||
|
<script src="//unpkg.com/alpinejs" defer></script>
|
||||||
|
<title>app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"
|
||||||
|
x-data="app">
|
||||||
|
<div id="root">
|
||||||
|
<div id="root-controls" x-data="{rootcontrols: true}">
|
||||||
|
<div id="toogle-root-controls">
|
||||||
|
<div id="theme-container">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="theme-switch"
|
||||||
|
class="checkbox"
|
||||||
|
@change="themeChange"
|
||||||
|
>
|
||||||
|
<label for="theme-switch" class="toggle" id="theme-toggle-label">
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p x-text="rootcontrols ? 'Ocultar Operaciones' : 'Mostrar Operaciones'"></p>
|
||||||
|
<button
|
||||||
|
id="toogle-root-contols-btn"
|
||||||
|
@click="rootcontrols = !rootcontrols"
|
||||||
|
x-html="rootcontrols ? '-' : '+'"
|
||||||
|
x-bind:class="rootcontrols ? 'opened' : 'closed'"
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
<div id="root-select-control" x-show="rootcontrols"
|
||||||
|
x-data="{
|
||||||
|
select: false,
|
||||||
|
compare: false,
|
||||||
|
|
||||||
|
}">
|
||||||
|
<button class=""
|
||||||
|
x-ref="historybtn"
|
||||||
|
hx-get="/api/control/select"
|
||||||
|
hx-target="#page"
|
||||||
|
@click="
|
||||||
|
select = true;
|
||||||
|
compare = false;
|
||||||
|
$refs.historybtn.disabled = true;
|
||||||
|
$refs.comparebtn.disabled = false;
|
||||||
|
"
|
||||||
|
x-bind:class="select ? 'opened' : ''"
|
||||||
|
>Historico</button>
|
||||||
|
<button class=""
|
||||||
|
x-ref="comparebtn"
|
||||||
|
hx-get="/api/control/compare"
|
||||||
|
hx-target="#page"
|
||||||
|
@click="
|
||||||
|
compare = true;
|
||||||
|
select = false;
|
||||||
|
$refs.historybtn.disabled = false;
|
||||||
|
$refs.comparebtn.disabled = true;
|
||||||
|
"
|
||||||
|
x-bind:class="compare ? 'opened' : ''"
|
||||||
|
>Comparar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="page">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
147
assets/templates/static/select-institution.html.tmpl
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
{{ define "hist-page" }}
|
||||||
|
<div id="hist-page"
|
||||||
|
x-data="{histcontrol: true, bancosselector: false, cajasselector: false, agentesselector: false }"
|
||||||
|
>
|
||||||
|
<div id="inst-selector">
|
||||||
|
<div id="toogle-hist-control">
|
||||||
|
<p x-text="histcontrol ? 'Ocultar Control e Instituciones' : 'Mostrar Control e Instituciones'"></p>
|
||||||
|
<button
|
||||||
|
id="toogle-hist-control-btn"
|
||||||
|
@click="histcontrol = ! histcontrol"
|
||||||
|
x-html="histcontrol ? '-' : '+'"
|
||||||
|
x-bind:class="histcontrol ? 'opened' : 'closed'"
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
<div id="inst-type-selector" x-show="histcontrol" >
|
||||||
|
<button
|
||||||
|
x-ref="bancosselectbtn"
|
||||||
|
class="select bancos"
|
||||||
|
hx-get="/api/bancos/?format=html&res=list"
|
||||||
|
hx-target="#response-div"
|
||||||
|
hx-trigger="click delay:500ms"
|
||||||
|
@click="
|
||||||
|
bancosselector = true;
|
||||||
|
cajasselector = false;
|
||||||
|
agentesselector = false;
|
||||||
|
$refs.bancosselectbtn.disabled = true;
|
||||||
|
$refs.cajasselectbtn.disabled = false;
|
||||||
|
$refs.agentesselectbtn.disabled = false;
|
||||||
|
"
|
||||||
|
x-bind:class="bancosselector ? 'opened': ''"
|
||||||
|
>Bancos</button>
|
||||||
|
<button
|
||||||
|
x-ref="cajasselectbtn"
|
||||||
|
class="select cajas"
|
||||||
|
hx-get="/api/cajas/?format=html&res=list"
|
||||||
|
hx-target="#response-div"
|
||||||
|
hx-trigger="click delay:500ms"
|
||||||
|
@click="
|
||||||
|
bancosselector = false;
|
||||||
|
cajasselector = true;
|
||||||
|
agentesselector = false;
|
||||||
|
$refs.bancosselectbtn.disabled = false;
|
||||||
|
$refs.cajasselectbtn.disabled = true;
|
||||||
|
$refs.agentesselectbtn.disabled = false;
|
||||||
|
"
|
||||||
|
x-bind:class="cajasselector ? 'opened': ''"
|
||||||
|
>Cajas</button>
|
||||||
|
<button
|
||||||
|
x-ref="agentesselectbtn"
|
||||||
|
class="select agentes"
|
||||||
|
hx-get="/api/agentes/?format=html&res=list"
|
||||||
|
hx-target="#response-div"
|
||||||
|
hx-trigger="click delay:500ms"
|
||||||
|
@click="
|
||||||
|
bancosselector = false;
|
||||||
|
cajasselector = false;
|
||||||
|
agentesselector = true;
|
||||||
|
$refs.bancosselectbtn.disabled = false;
|
||||||
|
$refs.cajasselectbtn.disabled = false;
|
||||||
|
$refs.agentesselectbtn.disabled = true;
|
||||||
|
"
|
||||||
|
x-bind:class="agentesselector ? 'opened': ''"
|
||||||
|
>Agentes</button>
|
||||||
|
</div>
|
||||||
|
<div id="response-div" x-show="histcontrol">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="response-inst-card" x-show="open">
|
||||||
|
<form x-ref="form"
|
||||||
|
x-bind:hx-post="urler"
|
||||||
|
hx-target="#response-inst-table-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="inst-selected">
|
||||||
|
<img
|
||||||
|
x-bind:style='instSelected === "banco vimenca" ? "background-color: #1789E1": ""'
|
||||||
|
x-bind:name="instSelected"
|
||||||
|
x-bind:src='instSelectedImage'
|
||||||
|
x-bind:hx-post="urler"
|
||||||
|
hx-trigger="load delay:500ms"
|
||||||
|
alt="bank logo" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<div>
|
||||||
|
<input id="inst-name-selector" hidden="true" name="name" x-bind:value="instSelected" required>
|
||||||
|
<input id="inst-res-selector" hidden="true" name="res" value="table">
|
||||||
|
<input id="inst-format-selector" hidden="true" name="format" value="html">
|
||||||
|
</div>
|
||||||
|
<div id="tf-container">
|
||||||
|
<input id="hours" type="radio" name="tf" value="hour" @change="e=>tf='hour'" >
|
||||||
|
<label for="hours">Horas</label>
|
||||||
|
<input id="days" type="radio" name="tf" value="day" @change="e=>tf='day'" required checked>
|
||||||
|
<label for="days">Dias</label>
|
||||||
|
<input id="weeks" type="radio" name="tf" value="week" @change="e=>tf='week'" >
|
||||||
|
<label for="weeks">Semanas</label>
|
||||||
|
<input id="months" type="radio" name="tf" value="month" @change="e=>tf='month'" >
|
||||||
|
<label for="months">Meses</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label id="time-amount-selector" for="amount">Cantidad <span id="ta-value" x-model="ta" required></span></label>
|
||||||
|
<input x-bind:hx-post="urler" hx-trigger="change delay:1.5s" @change="e => ta = e.target.value" x-ref="ta" type="range" id="ta-amount" name="ta" min="1" max="12" list="markers" x-bind:value="ta">
|
||||||
|
<datalist id="markers">
|
||||||
|
<option value="2"></option>
|
||||||
|
<option value="4"></option>
|
||||||
|
<option value="6"></option>
|
||||||
|
<option value="8"></option>
|
||||||
|
<option value="10"></option>
|
||||||
|
<option value="12"></option>
|
||||||
|
</datalist>
|
||||||
|
<input id="button-submit" type="submit" value="GO">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-text="queryRepr">Template</p>
|
||||||
|
<div
|
||||||
|
id="response-inst-table-container">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="response-table-graph">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "compare-page" }}
|
||||||
|
<div id="compare-page" x-data="{comparerpage: true}">
|
||||||
|
<div id="toogleinst">
|
||||||
|
<p x-text="comparerpage ? 'Ocultar Instituciones' : 'Mostrar Instituciones'">Mostrar Instituciones</p>
|
||||||
|
<button
|
||||||
|
id="toogle-inst-btn"
|
||||||
|
@click="comparerpage = !comparerpage"
|
||||||
|
x-bind:class="comparerpage ? 'opened' : 'closed'"
|
||||||
|
x-html="comparerpage ? '-' : '+'"
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
<div id="response-institution" hx-get="/api/all/?format=html&res=list" hx-trigger="load delay:1s" >
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
83
cmd/api/main.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/maximotejeda/us_dop_api/adapters/dolar"
|
||||||
|
"github.com/maximotejeda/us_dop_api/handlers"
|
||||||
|
"github.com/maximotejeda/us_dop_api/middlewares"
|
||||||
|
"github.com/maximotejeda/us_dop_api/static"
|
||||||
|
"github.com/maximotejeda/us_dop_bot/config"
|
||||||
|
"github.com/maximotejeda/us_dop_db/db"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hostSTR = os.Getenv("HOST)")
|
||||||
|
portSTR = os.Getenv("PORT")
|
||||||
|
templ template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// Log creation
|
||||||
|
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
|
||||||
|
|
||||||
|
var opts []grpc.DialOption
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
dolarConn, err := grpc.Dial(config.GetDollarServiceURL(), opts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating gerpc conn", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dol, err := dolar.NewAdapter(dolarConn)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating service adapter", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// templates
|
||||||
|
tmplDirStr := "assets/templates/"
|
||||||
|
ftmpl := os.DirFS(tmplDirStr)
|
||||||
|
toParseFiles := []string{}
|
||||||
|
|
||||||
|
// wlaks the Fs looking for templates
|
||||||
|
err = fs.WalkDir(ftmpl, ".", func(p string, d fs.DirEntry, err error) error {
|
||||||
|
log.Info("parsing", "file", p)
|
||||||
|
if filepath.Ext(p) == ".tmpl" {
|
||||||
|
toParseFiles = append(toParseFiles, tmplDirStr+p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init database
|
||||||
|
dbe := db.Dial("dolardb/crawler.db", log)
|
||||||
|
|
||||||
|
// handler containing all the necessary files
|
||||||
|
root := handlers.NewRoot(dbe, dol, log, toParseFiles)
|
||||||
|
|
||||||
|
s := middlewares.NewLogger(http.HandlerFunc(static.ServeFiles), log)
|
||||||
|
r := middlewares.NewLogger(root, log)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := http.Server{
|
||||||
|
Addr: hostSTR + ":" + portSTR,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
mux.Handle("/", r)
|
||||||
|
|
||||||
|
mux.Handle("GET /static/", s)
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
log.Error("while serving", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
25
config/config.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetNatsURI() string {
|
||||||
|
return getEnvVariable("NATSURI")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDollarServiceURL() string {
|
||||||
|
return getEnvVariable("DOLLAR_SERVICE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvironment() string {
|
||||||
|
return getEnvVariable("ENV")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvVariable(key string) string {
|
||||||
|
if os.Getenv(key) == "" {
|
||||||
|
log.Fatal("error getting key", key)
|
||||||
|
}
|
||||||
|
return os.Getenv(key)
|
||||||
|
}
|
||||||
17
domain/history.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type History struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Institution Institution `json:"institution"`
|
||||||
|
Compra float64 `json:"compra,omitempty"`
|
||||||
|
Venta float64 `json:"venta,omitempty"`
|
||||||
|
Parser string `json:"parser,omitempty"`
|
||||||
|
Parsed string `json:"parsed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Institution struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
}
|
||||||
13
go.mod
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module github.com/maximotejeda/us_dop_api
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8 // indirect
|
||||||
|
golang.org/x/net v0.24.0 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
|
||||||
|
google.golang.org/grpc v1.63.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.0 // indirect
|
||||||
|
)
|
||||||
14
go.sum
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8 h1:ldphxrQiAhmctWBMCaNShDphZfHmOeKuoSWwCxV62Ho=
|
||||||
|
github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8/go.mod h1:bAs0mlC1Vyn/BkHONL2Ik8ox9px9s9bhbJWgUQFMMWo=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||||
|
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||||
|
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||||
|
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
|
||||||
|
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
52
handlers/api/api.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maximotejeda/us_dop_api/domain"
|
||||||
|
"github.com/maximotejeda/us_dop_api/ports"
|
||||||
|
"github.com/maximotejeda/us_dop_db/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type API struct {
|
||||||
|
http.ServeMux
|
||||||
|
db *db.DB
|
||||||
|
dolar ports.DolarService
|
||||||
|
log *slog.Logger
|
||||||
|
templ template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResult struct {
|
||||||
|
InstRequested string `json:"institution_requested"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
TimeAmount string `json:"time_amount"`
|
||||||
|
TimeFrame string `json:"time_frame"`
|
||||||
|
Results []*domain.History `josn:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApiHandler(db *db.DB, dolar ports.DolarService, log *slog.Logger, templ template.Template) *API {
|
||||||
|
log = log.With("api", "insti")
|
||||||
|
api := &API{
|
||||||
|
db: db,
|
||||||
|
log: log,
|
||||||
|
dolar: dolar,
|
||||||
|
templ: templ,
|
||||||
|
}
|
||||||
|
api.HandleFunc("POST /api/instituciones/history", api.GetInstitutionHistory) // general handler to get banks, agents and others histories
|
||||||
|
// api.HandleFunc("GET /api/instituciones/{inst}", nil) // return the card of just one institution with details
|
||||||
|
api.HandleFunc("GET /api/{inst}/", api.GetInstNames)
|
||||||
|
api.HandleFunc("GET /api/latest/all", api.GetAllLatestPrice)
|
||||||
|
api.HandleFunc("GET /api/control/select", api.GetHistoryPage)
|
||||||
|
api.HandleFunc("GET /api/control/compare", api.GetComparisonPage)
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
// instParseHTML
|
||||||
|
//
|
||||||
|
// Can be much tipe of html part of a list, a form, a card etc...
|
||||||
|
func instParseHTML(SearchResult) string {
|
||||||
|
return "html"
|
||||||
|
}
|
||||||
11
handlers/api/constrols.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (api *API) GetHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.templ.ExecuteTemplate(w, "hist-page", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetComparisonPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.templ.ExecuteTemplate(w, "compare-page", nil)
|
||||||
|
}
|
||||||
226
handlers/api/institution.go
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maximotejeda/us_dop_api/domain"
|
||||||
|
"github.com/maximotejeda/us_dop_api/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handler to parse query to the api
|
||||||
|
func (api *API) GetInstitutionHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
timeSearch time.Duration
|
||||||
|
historyResult []*domain.History
|
||||||
|
)
|
||||||
|
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("parsing form ", "error", err)
|
||||||
|
http.Error(w, "parsing form", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
queryP := r.Form
|
||||||
|
timeAmount := queryP.Get("ta") // numeric amount
|
||||||
|
timeFrame := queryP.Get("tf") // minute, hour, day, week, month, now ago until now
|
||||||
|
resType := queryP.Get("format") // can be multiples html or a json
|
||||||
|
|
||||||
|
inst := queryP.Get("name")
|
||||||
|
resource := queryP.Get("res")
|
||||||
|
inst = strings.ToLower(inst)
|
||||||
|
// match time amount number
|
||||||
|
re := regexp.MustCompile(`[0-9]{1,3}`)
|
||||||
|
// api.log.Info("parsing inst", "name", inst)
|
||||||
|
if timeFrame != "now" {
|
||||||
|
if !re.Match([]byte(timeAmount)) {
|
||||||
|
api.log.Error("matching time", "error", "incorrect time amount")
|
||||||
|
http.Error(w, "incorrect time amount", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSearch, err = helpers.ParseTimeAmount(timeFrame, timeAmount)
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("parsing time ", "error", err)
|
||||||
|
http.Error(w, "incorrect time amount", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeFrame == "now" {
|
||||||
|
hist, err := api.dolar.GetLatest(inst)
|
||||||
|
//hist, err := api.db.GetLastPrice(inst)
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("GetLastPrice", "time Frame", "now", "error", err)
|
||||||
|
http.Error(w, "price not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
historyResult = append(historyResult, hist)
|
||||||
|
} else {
|
||||||
|
historyResult, err = api.dolar.GetSince(inst, int64(timeSearch.Minutes()))
|
||||||
|
//historyResult, err = api.db.GetChangeSince(inst, timeSearch)
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("GetLastPrice", "time Frame", timeFrame, "amount", timeAmount, "error", err)
|
||||||
|
http.Error(w, "price not found for time frame", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.log.Info("resultados", "=", historyResult)
|
||||||
|
}
|
||||||
|
searchRes := SearchResult{
|
||||||
|
InstRequested: inst,
|
||||||
|
Date: time.Now(),
|
||||||
|
TimeAmount: timeAmount,
|
||||||
|
TimeFrame: timeFrame,
|
||||||
|
Results: historyResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we want a json
|
||||||
|
if resType == "html" {
|
||||||
|
switch resource {
|
||||||
|
case "card":
|
||||||
|
err = api.templ.ExecuteTemplate(w, "card-history", searchRes)
|
||||||
|
case "list":
|
||||||
|
// by default we crate simple query
|
||||||
|
// we need a way to distinguish from mixed or more complex query
|
||||||
|
err = api.templ.ExecuteTemplate(w, "list-history", searchRes)
|
||||||
|
case "table":
|
||||||
|
// by default we crate simple query
|
||||||
|
// we need a way to distinguish from mixed or more complex query
|
||||||
|
err = api.templ.ExecuteTemplate(w, "table-history", searchRes)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("History", "error", err)
|
||||||
|
http.Error(w, "history error", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, _ := json.Marshal(searchRes)
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBanks
|
||||||
|
// as bank will only return a list of names
|
||||||
|
// we can assume we need a list of some html to the app
|
||||||
|
func (api *API) GetInstNames(w http.ResponseWriter, r *http.Request) {
|
||||||
|
queryP := r.URL.Query()
|
||||||
|
fType := queryP.Get("format")
|
||||||
|
resource := queryP.Get("res")
|
||||||
|
inst := r.PathValue("inst")
|
||||||
|
|
||||||
|
w.Header().Set("Etag", "institutions")
|
||||||
|
w.Header().Set("Cache-Control", "max-age=3600")
|
||||||
|
|
||||||
|
var institutions, err = []string{}, *new(error)
|
||||||
|
switch inst {
|
||||||
|
case "bancos", "banco":
|
||||||
|
inst = "bancos"
|
||||||
|
institutions, err = api.dolar.GetInstByType("bancos")
|
||||||
|
case "caja", "cajas":
|
||||||
|
inst = "cajas"
|
||||||
|
institutions, err = api.dolar.GetInstByType("cajas")
|
||||||
|
case "agente", "agentes":
|
||||||
|
inst = "agentes"
|
||||||
|
institutions, err = api.dolar.GetInstByType("agentes")
|
||||||
|
case "all":
|
||||||
|
bancos, _ := api.dolar.GetInstByType("bancos")
|
||||||
|
cajas, _ := api.dolar.GetInstByType("cajas")
|
||||||
|
agentes, _ := api.dolar.GetInstByType("agentes")
|
||||||
|
inst = "all"
|
||||||
|
institutions = slices.Concat(bancos, cajas, agentes)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("GetInstName", "error", err)
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
searchResult := SearchResult{
|
||||||
|
InstRequested: inst,
|
||||||
|
Date: time.Now(),
|
||||||
|
TimeAmount: "none",
|
||||||
|
TimeFrame: "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDup := map[string]int{}
|
||||||
|
res := []string{}
|
||||||
|
for _, val := range institutions {
|
||||||
|
removeDup[val]++
|
||||||
|
if removeDup[val] == 1 {
|
||||||
|
res = append(res, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, banco := range res {
|
||||||
|
inst := domain.History{}
|
||||||
|
inst.Institution.Name = banco
|
||||||
|
searchResult.Results = append(searchResult.Results, &inst)
|
||||||
|
}
|
||||||
|
api.log.Info("institutions", "list", res)
|
||||||
|
if fType == "html" {
|
||||||
|
switch resource {
|
||||||
|
case "card":
|
||||||
|
err = api.templ.ExecuteTemplate(w, "card", searchResult)
|
||||||
|
case "list":
|
||||||
|
// by default we crate simple query
|
||||||
|
// we need a way to distinguish from mixed or more complex query
|
||||||
|
err = api.templ.ExecuteTemplate(w, "list", searchResult)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("rendering institucion", "tipo", inst, "error", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dat, err := json.Marshal(searchResult)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
w.Write(dat)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetLatestPrice(w http.ResponseWriter, r *http.Request) {
|
||||||
|
inst := r.PathValue("inst")
|
||||||
|
if inst == "" {
|
||||||
|
http.Error(w, "no inst in request", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
price, err := api.dolar.GetLatest(inst)
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("geting latest price", "error", err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
api.templ.ExecuteTemplate(w, "latest-price", price)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetAllLatestPrice(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bancos, err := api.dolar.GetInstByType("bancos")
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("geting inst", "error", err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
cajas, err := api.dolar.GetInstByType("cajas")
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("geting inst cajas", "error", err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
agentes, err := api.dolar.GetInstByType("agentes")
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("geting inst agentes", "error", err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
instLastPriceList := []*domain.History{}
|
||||||
|
for _, inst := range slices.Concat(bancos, cajas, agentes) {
|
||||||
|
in, err := api.dolar.GetLatest(inst)
|
||||||
|
if err != nil {
|
||||||
|
api.log.Error("geting last price", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instLastPriceList = append(instLastPriceList, in)
|
||||||
|
}
|
||||||
|
api.templ.ExecuteTemplate(w, "latest-price", instLastPriceList)
|
||||||
|
}
|
||||||
1
handlers/app/app.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package app
|
||||||
87
handlers/handlers.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maximotejeda/us_dop_api/handlers/api"
|
||||||
|
"github.com/maximotejeda/us_dop_api/ports"
|
||||||
|
"github.com/maximotejeda/us_dop_db/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
templ template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mux will mix and match all sub routes
|
||||||
|
type Root struct {
|
||||||
|
http.ServeMux
|
||||||
|
db *db.DB
|
||||||
|
log *slog.Logger
|
||||||
|
dolar ports.DolarService
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoot(dbe *db.DB, dolar ports.DolarService, log *slog.Logger, files []string) *Root {
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"add": add[float64],
|
||||||
|
"div": div,
|
||||||
|
"mult": mult[float64],
|
||||||
|
"fdate": fdate,
|
||||||
|
"replace": replace,
|
||||||
|
}
|
||||||
|
templ = *template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
||||||
|
r := Root{
|
||||||
|
log: log,
|
||||||
|
dolar: dolar,
|
||||||
|
templates: &templ,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templ.ExecuteTemplate(w, "index", nil)
|
||||||
|
//w.Write([]byte("{'status': 'ok'}"))
|
||||||
|
})
|
||||||
|
inst := api.NewApiHandler(dbe, dolar, log, templ)
|
||||||
|
r.Handle("GET /api/{inst}/", inst)
|
||||||
|
r.Handle("POST /api/instituciones/history", inst)
|
||||||
|
r.Handle("GET /api/latest/{inst}", inst)
|
||||||
|
r.Handle("POST /api/control/", inst)
|
||||||
|
r.HandleFunc("/api/{$}", http.NotFound)
|
||||||
|
|
||||||
|
// telegram
|
||||||
|
r.HandleFunc("/telegram/{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templ.ExecuteTemplate(w, "index", nil)
|
||||||
|
//w.Write([]byte("{'status': 'ok'}"))
|
||||||
|
})
|
||||||
|
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
func add[v float64 | int](a, b v) v {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
func div(a float64, b int) float64 {
|
||||||
|
if a == 0 || b == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a / float64(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mult[v float64 | int](a, b v) v {
|
||||||
|
return a * b
|
||||||
|
}
|
||||||
|
|
||||||
|
func fdate(t time.Time) string {
|
||||||
|
|
||||||
|
return t.Format("15 02-01-06")
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace
|
||||||
|
// replace string
|
||||||
|
func replace(rep, torep, original string) string {
|
||||||
|
return strings.ReplaceAll(original, rep, torep)
|
||||||
|
}
|
||||||
27
helpers/timehelpers.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseTimeAmount(timeFrame, timeAmount string) (timeSearch time.Duration, err error) {
|
||||||
|
timeInt, err := strconv.ParseInt(strings.ReplaceAll(timeAmount, "-", ""), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return timeSearch, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch timeFrame {
|
||||||
|
case "hour":
|
||||||
|
timeSearch = 60 * 1 * time.Duration(timeInt) * time.Hour
|
||||||
|
case "day":
|
||||||
|
timeSearch = 60 * 24 * time.Duration(timeInt) * time.Hour
|
||||||
|
case "week":
|
||||||
|
timeSearch = (60 * 24 * 7 * time.Duration(timeInt)) * time.Hour
|
||||||
|
case "month":
|
||||||
|
timeSearch = (60 * 24 * 7 * 4) * time.Hour
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return timeSearch, err
|
||||||
|
}
|
||||||
73
k8s/deployment.yml.template
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: us-dop-api
|
||||||
|
labels:
|
||||||
|
app: us-dop-api
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: us-dop-api
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: us-dop-api
|
||||||
|
name: us-dop-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: us-dop-pi
|
||||||
|
image: localhost:32000/us-dop-api:latest
|
||||||
|
env:
|
||||||
|
- name: DOLLAR_SERVICE_URL
|
||||||
|
value: "dolar-grpc-svc:80"
|
||||||
|
- name: HOST
|
||||||
|
value: "0.0.0.0"
|
||||||
|
- name: NATSURI
|
||||||
|
value: "nats://nats-svc:4222"
|
||||||
|
- name: PORT
|
||||||
|
value: "8080"
|
||||||
|
volumeMounts:
|
||||||
|
- name: database
|
||||||
|
mountPath: /app/dolardb
|
||||||
|
volumes:
|
||||||
|
- name: database
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: bank-crawler-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: us-dop-api-svc
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app: us-dop-api
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
name: frontend
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
# add an annotation indicating the issuer to use.
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt
|
||||||
|
name: us-dop-api-ingress
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: cambio.maximotejeda.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: us-dop-api-svc
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
tls: # < placing a host in the TLS config will determine what ends up in the cert's subjectAltNames
|
||||||
|
- hosts:
|
||||||
|
- cambio.maximotejeda.com
|
||||||
|
secretName: cambio.maximotejeda-cert # < cert-manager will store the created certificate in this secret.
|
||||||
36
k8s/deployment.yml.template.old
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: us-dop-bot
|
||||||
|
labels:
|
||||||
|
app: us-dop-bot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: us-dop-bot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: us-dop-bot
|
||||||
|
name: us-dop-bot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: us-dop-bot
|
||||||
|
image: localhost:32000/us-dop-bot:latest
|
||||||
|
env:
|
||||||
|
- name: DBURINST
|
||||||
|
value: $DBURINST
|
||||||
|
- name: DBURIUSER
|
||||||
|
value: $DBURIUSER
|
||||||
|
- name: NATSURI
|
||||||
|
value: "nats://nats-svc:4222"
|
||||||
|
- name: TOKEN
|
||||||
|
value: "$TOKEN"
|
||||||
|
volumeMounts:
|
||||||
|
- name: database
|
||||||
|
mountPath: /app/dolardb
|
||||||
|
volumes:
|
||||||
|
- name: database
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: bank-crawler-pvc
|
||||||
63
middlewares/logs.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logMiddleware struct {
|
||||||
|
handler http.Handler
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(h http.Handler, log *slog.Logger) *logMiddleware {
|
||||||
|
return &logMiddleware{
|
||||||
|
handler: h,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
rec := statusRecorder{w, 200}
|
||||||
|
l.handler.ServeHTTP(&rec, r)
|
||||||
|
if rec.status < 299 {
|
||||||
|
l.log.Info(
|
||||||
|
"incoming request",
|
||||||
|
"status", rec.status,
|
||||||
|
"method", r.Method,
|
||||||
|
"time_spent_ms", time.Since(start).Milliseconds(),
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"user_agent", r.Header.Get("User-Agent"),
|
||||||
|
)
|
||||||
|
} else if rec.status > 299 && rec.status < 399 {
|
||||||
|
l.log.Warn(
|
||||||
|
"redirecting request",
|
||||||
|
"status", rec.status,
|
||||||
|
"method", r.Method,
|
||||||
|
"time_spent_ms", time.Since(start).Milliseconds(),
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"user_agent", r.Header.Get("User-Agent"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
l.log.Warn(
|
||||||
|
"error on request",
|
||||||
|
"status", rec.status,
|
||||||
|
"method", r.Method,
|
||||||
|
"time_spent_ms", time.Since(start).Milliseconds(),
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"user_agent", r.Header.Get("User-Agent"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *statusRecorder) WriteHeader(code int) {
|
||||||
|
rec.status = code
|
||||||
|
rec.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
1
middlewares/middlewares.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package middlewares
|
||||||
13
ports/dolar.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package ports
|
||||||
|
|
||||||
|
import "github.com/maximotejeda/us_dop_api/domain"
|
||||||
|
|
||||||
|
type DolarService interface {
|
||||||
|
GetLatest(name string) (*domain.History, error)
|
||||||
|
GetSince(name string, duration int64) ([]*domain.History, error)
|
||||||
|
GetInstByType(name string) ([]string, error)
|
||||||
|
Subscribe(int64, string) (bool, error)
|
||||||
|
Unsubscribe(int64, string) (bool, error)
|
||||||
|
GetSubscribedUsers(string) ([]int64, error)
|
||||||
|
GetSubscribedInsts(int64) ([]string, error)
|
||||||
|
}
|
||||||
37
static/static.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// package containing the static server
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServeFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := r.URL.Path
|
||||||
|
pl := strings.Split(path, ".")
|
||||||
|
l := len(pl)
|
||||||
|
reg := regexp.MustCompile(`.*\.(png|jpg|jpeg|css|js|html|gz)$`)
|
||||||
|
switch {
|
||||||
|
case l <= 1:
|
||||||
|
fmt.Println("path not found " + r.URL.Path)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
case l >= 2:
|
||||||
|
if ok := reg.Match([]byte(r.URL.Path)); ok {
|
||||||
|
if pl[len(pl)-1] == "gz" {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
hs := http.FileServer(http.Dir("./assets"))
|
||||||
|
http.StripPrefix("/static/", hs).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||