Compare commits
62 Commits
Author | SHA1 | Date |
---|---|---|
|
1fdbf56aa7 | |
|
79eee41365 | |
|
9b938dad97 | |
|
5ccd796e1b | |
|
9189d42630 | |
|
8aeb4a1759 | |
|
c7ffa15db1 | |
|
d64e899e0f | |
|
7c6bac1986 | |
|
5b89c97b00 | |
|
d8524c347e | |
|
c54e147173 | |
|
92c0cb4de0 | |
|
b4ccdeff2f | |
|
48c1529e6c | |
|
674cdff87b | |
|
3a7d454826 | |
|
50548c29ab | |
|
e5253f9adb | |
|
e425b88477 | |
|
c84b58a0d5 | |
|
5d4fe15e88 | |
|
b2ee4dfea3 | |
|
7edf3a3ed1 | |
|
f71ad2cc65 | |
|
90d8247c0f | |
|
2299f2325e | |
|
ff9f33dfba | |
|
205d1f1e99 | |
|
d0f6c9734a | |
|
17f7520876 | |
|
3559ff311b | |
|
846ca0bc5c | |
|
f2b24a83a3 | |
|
c9e8165f83 | |
|
30e87c309e | |
|
9eb6483cb9 | |
|
2d4055b653 | |
|
b7e7d79177 | |
|
fdf9502c8b | |
|
3aa53cf1a9 | |
|
598354e8b7 | |
|
dab19bbc4d | |
|
0e22096447 | |
|
a26e9c6e12 | |
|
7eb718dfd9 | |
|
cba892c4c0 | |
|
cdd91c815e | |
|
bc5fd61d5d | |
|
23d16fa162 | |
|
e726bde025 | |
|
746aa013d3 | |
|
b291ac34fc | |
|
6c18529317 | |
|
75c94a95f5 | |
|
72f8a329d2 | |
|
d71d974abd | |
|
1b46f4224e | |
|
fc9ac321d5 | |
|
dbbd06cf85 | |
|
0412ffca05 | |
|
cc1b334639 |
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Temperature struct {
|
||||
Day float64 `json:"day"`
|
||||
Min float64 `json:"min"`
|
||||
}
|
||||
|
||||
type Weather struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type Forecast struct {
|
||||
Timestamp int64 `json:"dt"`
|
||||
Weather []Weather `json:"weather"`
|
||||
Temperature Temperature `json:"temp"`
|
||||
}
|
||||
|
||||
type OpenWeatherMap struct {
|
||||
Forecasts []Forecast `json:"list"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := database.New(context.Background(), os.Getenv("CAMPER_DATABASE_URL"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Acquire(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Release()
|
||||
updateForecast(context.Background(), conn)
|
||||
}
|
||||
|
||||
func updateForecast(ctx context.Context, conn *database.Conn) {
|
||||
if _, err := conn.Exec(ctx, "set role to admin"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var stationURL string
|
||||
if err := conn.QueryRow(ctx, `
|
||||
select station_uri
|
||||
from weather_forecast
|
||||
`).Scan(
|
||||
&stationURL,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(stationURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var result OpenWeatherMap
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, forecast := range result.Forecasts {
|
||||
if _, err := conn.Exec(ctx, `
|
||||
update weather_forecast
|
||||
set weather_condition_id = $1
|
||||
, day_temperature = $2
|
||||
, min_temperature = $3
|
||||
, forecasted_at = $4
|
||||
, updated_at = current_timestamp
|
||||
where station_uri = $5
|
||||
`,
|
||||
strconv.Itoa(forecast.Weather[0].ID),
|
||||
forecast.Temperature.Day,
|
||||
forecast.Temperature.Min,
|
||||
time.Unix(forecast.Timestamp, 0),
|
||||
stationURL,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
42 * * * * camper /usr/bin/psql --quiet --output=/dev/null --command='set role guest; select * from camper.flush_payments()'
|
||||
21 * * * * camper /usr/bin/camper-weather
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
usr/bin/camper usr/bin
|
||||
usr/bin/camper-weather usr/bin
|
||||
locale usr/share/camper
|
||||
web usr/share/camper
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
camper (1.5~git00000000000000.0000000-1) bookworm; urgency=medium
|
||||
camper (1.8~git00000000000000.0000000-1) bookworm; urgency=medium
|
||||
|
||||
* Initial release
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ Build-Depends:
|
|||
golang-any,
|
||||
golang-github-jackc-pgx-v4-dev,
|
||||
golang-github-leonelquinteros-gotext-dev,
|
||||
golang-github-rainycape-unidecode-dev,
|
||||
golang-golang-x-text-dev,
|
||||
postgresql-all (>= 217~),
|
||||
sqitch,
|
||||
|
@ -45,7 +46,8 @@ Architecture: any
|
|||
Depends:
|
||||
${shlibs:Depends},
|
||||
${misc:Depends},
|
||||
adduser
|
||||
adduser,
|
||||
weasyprint
|
||||
Built-Using: ${misc:Built-Using}
|
||||
Description: Simple campground reservation management application
|
||||
A simple web application to manage reservations to campgrounds, intended for
|
||||
|
|
|
@ -10,8 +10,8 @@ values ('demo@camper', 'Demo User', 'demo', 'ca')
|
|||
;
|
||||
|
||||
alter table company alter column company_id restart with 52;
|
||||
insert into company (slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, rtc_number, tourist_tax, currency_code, default_lang_tag, legal_disclaimer)
|
||||
values ('09184122-b276-4be2-9553-e4bbcbafe40d', 'El pont de Llierca, S.L.', 'ESB17377656', 'Càmping Montagut', parse_packed_phone_number('661 673 057', 'ES'), 'info@campingmontagut.com', 'https://campingmontagut.com/', 'Ctra. de Sadernes, Km 2', 'Montagut i Oix', 'Girona', '17855', 'ES', 'KG-000133', 60, 'EUR', 'ca', 'El pont de Llierca, S.L. és responsable del tractament de les seves dades d’acord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets d’accés, rectificació, portabilitat, supressió, limitació i oposició a El pont de Llierca, S.L., amb domicili Ctra. de Sadernes, Km 2, 17855 Montagut i Oix o enviant un correu electrònic a info@campingmontagut.com. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a campingmontagut.com.');
|
||||
insert into company (slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, rtc_number, tourist_tax, tourist_tax_max_days, currency_code, default_lang_tag, legal_disclaimer)
|
||||
values ('09184122-b276-4be2-9553-e4bbcbafe40d', 'El pont de Llierca, S.L.', 'ESB17377656', 'Càmping Montagut', parse_packed_phone_number('661 673 057', 'ES'), 'jordi@tandem.blog', 'https://tandem.blog/', 'Ctra. de Sadernes, Km 2', 'Montagut i Oix', 'Girona', '17855', 'ES', 'KG-000133', 60, 7, 'EUR', 'ca', 'El pont de Llierca, S.L. és responsable del tractament de les seves dades d’acord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets d’accés, rectificació, portabilitat, supressió, limitació i oposició a El pont de Llierca, S.L., amb domicili Ctra. de Sadernes, Km 2, 17855 Montagut i Oix o enviant un correu electrònic a jordi@tandem.blog. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a tandem.blog.');
|
||||
|
||||
insert into company_host (company_id, host)
|
||||
values (52, 'localhost:8080')
|
||||
|
@ -24,6 +24,21 @@ values (52, 42, 'employee')
|
|||
, (52, 43, 'admin')
|
||||
;
|
||||
|
||||
insert into payment_method (payment_method_id, company_id, name, instructions)
|
||||
values (1, 52, 'Pagament', '')
|
||||
;
|
||||
|
||||
insert into tax_class (tax_class_id, company_id, name)
|
||||
values (1, 52, 'VAT')
|
||||
;
|
||||
|
||||
insert into tax (tax_id, company_id, tax_class_id, name, rate)
|
||||
values (1, 52, 1, 'General VAT (21 %)', 0.21)
|
||||
, (2, 52, 1, 'Reduced VAT (10 %)', 0.10)
|
||||
, (3, 52, 1, 'Super-reduced VAT (4 %)', 0.04)
|
||||
, (4, 52, 1, 'VAT free (0 %)', 0.00)
|
||||
;
|
||||
|
||||
select setup_redsys(52, '361716962', '1', 'test', 'redirect', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7');
|
||||
|
||||
select setup_location(52, '<div><h3>On som</h3><p>Ctra. de Sadernes, km 2, 17855 MONTAGUT i OIX</p></div>', '<iframe src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d44661.89614700166!2d2.57381383167473!3d42.24000148364468!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12bab79ff3b0f007%3A0x65b7563a5d1548e6!2sCamping%20Montagut!5e0!3m2!1sca!2sus!4v1703225042845!5m2!1sca!2sus" width="100%" height="600" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>', '<div><p><strong>Càmping i Safari tents:</strong><br />de 08/04 a 09/10</p><p><strong>Cabanes i Bungalows:</strong><br />de 08/04 a 11/12</p><p><strong>ACSI</strong>:<br />de 08/04 a 11/12</p></div>');
|
||||
|
@ -1233,6 +1248,15 @@ select set_season_range(93, daterange(make_date(extract(year from current_date):
|
|||
select set_season_range(94, daterange(make_date(extract(year from current_date)::int, 9, 24), make_date(extract(year from current_date)::int, 9, 29)));
|
||||
select set_season_range(93, daterange(make_date(extract(year from current_date)::int, 9, 29), make_date(extract(year from current_date)::int, 10, 1)));
|
||||
select set_season_range(94, daterange(make_date(extract(year from current_date)::int, 10, 1), make_date(extract(year from current_date)::int, 10, 13)));
|
||||
select set_season_range(92, daterange(make_date(extract(year from current_date)::int + 1, 4, 11), make_date(extract(year from current_date)::int + 1, 4, 22)));
|
||||
select set_season_range(94, daterange(make_date(extract(year from current_date)::int + 1, 4, 22), make_date(extract(year from current_date)::int + 1, 6, 20)));
|
||||
select set_season_range(92, daterange(make_date(extract(year from current_date)::int + 1, 6, 20), make_date(extract(year from current_date)::int + 1, 6, 25)));
|
||||
select set_season_range(93, daterange(make_date(extract(year from current_date)::int + 1, 6, 25), make_date(extract(year from current_date)::int + 1, 7, 4)));
|
||||
select set_season_range(92, daterange(make_date(extract(year from current_date)::int + 1, 7, 4), make_date(extract(year from current_date)::int + 1, 8, 25)));
|
||||
select set_season_range(93, daterange(make_date(extract(year from current_date)::int + 1, 8, 25), make_date(extract(year from current_date)::int + 1, 9, 1)));
|
||||
select set_season_range(94, daterange(make_date(extract(year from current_date)::int + 1, 9, 1), make_date(extract(year from current_date)::int + 1, 9, 11)));
|
||||
select set_season_range(92, daterange(make_date(extract(year from current_date)::int + 1, 9, 11), make_date(extract(year from current_date)::int + 1, 9, 15)));
|
||||
select set_season_range(94, daterange(make_date(extract(year from current_date)::int + 1, 9, 15), make_date(extract(year from current_date)::int + 1, 12, 9)));
|
||||
|
||||
select set_campsite_type_cost (slug, 92, '4.00', '7.95', '7.95', '6.40') from campsite_type where campsite_type_id = 72;
|
||||
select set_campsite_type_cost (slug, 93, '2.00', '7.40', '7.40', '5.90') from campsite_type where campsite_type_id = 72;
|
||||
|
@ -1413,6 +1437,22 @@ values (72, 77, 'en', 'Legend')
|
|||
, (76, 103, 'es', 'Leyenda')
|
||||
;
|
||||
|
||||
insert into acsi (campsite_type_id, number_adults, number_teenagers, number_children, number_dogs, cost_per_night)
|
||||
values (72, 2, 0, 0, 1, 2300);
|
||||
|
||||
insert into acsi_calendar (campsite_type_id, acsi_range)
|
||||
values (72, daterange(make_date(extract(year from current_date)::int, 2, 4), make_date(extract(year from current_date)::int, 6, 20)))
|
||||
, (72, daterange(make_date(extract(year from current_date)::int, 9, 1), make_date(extract(year from current_date)::int, 10, 13)))
|
||||
;
|
||||
|
||||
insert into acsi_option (campsite_type_id, campsite_type_option_id, units, option_group)
|
||||
values (72, 102, 1, 1)
|
||||
, (72, 103, 1, 2)
|
||||
, (72, 104, 1, 3)
|
||||
, (72, 107, 1, 4)
|
||||
, (72, 109, 1, 5)
|
||||
;
|
||||
|
||||
alter table surroundings_highlight alter column surroundings_highlight_id restart with 112;
|
||||
|
||||
select add_surroundings_highlight(52, 62, 'El Pont del Llierca', '<p>Pont d’estil romànic i bany natural a 400 m del càmping.</p>');
|
||||
|
@ -1471,19 +1511,34 @@ select translate_surroundings_ad(52, 'es', '¡Ven a hacer barranquismo en Sadern
|
|||
select translate_surroundings_ad(52, 'fr', 'Venez faire du canyoning à Sadernes !', 'Réservez votre journée');
|
||||
|
||||
alter table booking alter column booking_id restart with 122;
|
||||
|
||||
insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card, booking_status)
|
||||
values (52, 72, 'Juli Verd', current_date + interval '23 days', current_date + interval '25 days', 0, false, 'created')
|
||||
, (52, 72, 'Pere Gil', current_date + interval '24 days', current_date + interval '25 days', 1, true, 'cancelled')
|
||||
, (52, 73, 'Calèndula Groga', current_date + interval '24 days', current_date + interval '25 days', 0, false, 'confirmed')
|
||||
, (52, 73, 'Rosa Blava', current_date + interval '15 days', current_date + interval '22 days', 0, false, 'checked-in')
|
||||
, (52, 74, 'Margarita Blanca', current_date + interval '7 days', current_date + interval '8 days', 0, false, 'invoiced')
|
||||
, (52, 74, 'Camèlia Vermella', current_date + interval '7 days', current_date + interval '8 days', 0, false, 'created')
|
||||
, (52, 74, 'Valeriana Rosa', current_date + interval '3 days', current_date + interval '8 days', 0, true, 'cancelled')
|
||||
, (52, 75, 'Jacint Violeta', current_date + interval '30 days', current_date + interval '33 days', 0, false, 'checked-in')
|
||||
, (52, 76, 'Hortènsia Grisa', current_date + interval '29 days', current_date + interval '34 days', 0, false, 'invoiced')
|
||||
insert into booking (company_id, campsite_type_id, holder_name, stay, zone_preferences, subtotal_nights, number_adults, subtotal_adults, number_teenagers, subtotal_teenagers, number_children, subtotal_children, number_dogs, subtotal_dogs, subtotal_tourist_tax, total, acsi_card, booking_status, currency_code)
|
||||
values (52, 72, 'Juli Verd', daterange((current_date + interval '23 days')::date, (current_date + interval '25 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'created', 'EUR')
|
||||
, (52, 72, 'Camèlia Vermella', daterange((current_date + interval '7 days')::date, (current_date + interval '8 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'created', 'EUR')
|
||||
, (52, 72, 'Margarita Blanca', daterange((current_date + interval '7 days')::date, (current_date + interval '8 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'invoiced', 'EUR')
|
||||
, (52, 72, 'Rosa Blava', daterange((current_date + interval '8 days')::date, (current_date + interval '11 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'checked-in', 'EUR')
|
||||
, (52, 72, 'Calèndula Groga', daterange((current_date + interval '14 days')::date, (current_date + interval '21 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'confirmed', 'EUR')
|
||||
, (52, 72, 'Jacint Violeta', daterange((current_date + interval '9 days')::date, (current_date + interval '13 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'checked-in', 'EUR')
|
||||
, (52, 72, 'Hortènsia Grisa', daterange((current_date + interval '4 days')::date, (current_date + interval '8 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'invoiced', 'EUR')
|
||||
, (52, 72, 'Pere Gil', daterange((current_date + interval '9 days')::date, (current_date + interval '19 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, true, 'confirmed', 'EUR')
|
||||
, (52, 72, 'Juli Verd', daterange((current_date + interval '11 days')::date, (current_date + interval '13 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'confirmed', 'EUR')
|
||||
, (52, 72, 'Camèlia Vermella', daterange((current_date + interval '13 days')::date, (current_date + interval '15 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'confirmed', 'EUR')
|
||||
, (52, 72, 'Valeriana Rosa', daterange((current_date + interval '15 days')::date, (current_date + interval '17 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 'confirmed', 'EUR')
|
||||
, (52, 72, 'Pere Gil', daterange((current_date + interval '24 days')::date, (current_date + interval '25 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, true, 'cancelled', 'EUR')
|
||||
, (52, 72, 'Valeriana Rosa', daterange((current_date + interval '3 days')::date, (current_date + interval '8 days')::date), '', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, true, 'cancelled', 'EUR')
|
||||
;
|
||||
|
||||
insert into booking_campsite (booking_id, campsite_id, stay)
|
||||
values (124, 90, daterange((current_date + interval '7 days')::date, (current_date + interval '8 days')::date))
|
||||
, (124, 94, daterange((current_date + interval '7 days')::date, (current_date + interval '8 days')::date))
|
||||
, (125, 90, daterange((current_date + interval '8 days')::date, (current_date + interval '11 days')::date))
|
||||
, (126, 90, daterange((current_date + interval '14 days')::date, (current_date + interval '21 days')::date))
|
||||
, (127, 91, daterange((current_date + interval '9 days')::date, (current_date + interval '13 days')::date))
|
||||
, (128, 92, daterange((current_date + interval '4 days')::date, (current_date + interval '8 days')::date))
|
||||
, (129, 93, daterange((current_date + interval '9 days')::date, (current_date + interval '19 days')::date))
|
||||
, (130, 94, daterange((current_date + interval '11 days')::date, (current_date + interval '13 days')::date))
|
||||
, (131, 94, daterange((current_date + interval '13 days')::date, (current_date + interval '15 days')::date))
|
||||
, (132, 94, daterange((current_date + interval '15 days')::date, (current_date + interval '17 days')::date))
|
||||
;
|
||||
|
||||
alter table amenity alter column amenity_id restart with 132;
|
||||
select add_amenity(52, 'camp-esport', 'Camp Esport', '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec scelerisque lorem vestibulum enim sollicitudin ornare. Aliquam egestas pretium porttitor. Donec iaculis tempus est, id lobortis risus semper vel. Maecenas ut imperdiet neque. Donec mattis purus felis, vitae interdum risus egestas pharetra. Vestibulum dui neque, condimentum ultrices erat sed, fringilla pharetra ante. Maecenas hendrerit neque mattis risus consectetur euismod. Cras urna metus, bibendum a neque sed, pharetra commodo magna.</p>', '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec scelerisque lorem vestibulum enim sollicitudin ornare. Aliquam egestas pretium porttitor. Donec iaculis tempus est, id lobortis risus semper vel. Maecenas ut imperdiet neque. Donec mattis purus felis, vitae interdum risus egestas pharetra. Vestibulum dui neque, condimentum ultrices erat sed, fringilla pharetra ante. Maecenas hendrerit neque mattis risus consectetur euismod. Cras urna metus, bibendum a neque sed, pharetra commodo magna.</p>');
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:acsi to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: campsite_type
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table acsi (
|
||||
campsite_type_id integer primary key references campsite_type,
|
||||
number_adults positive_integer not null,
|
||||
number_teenagers nonnegative_integer not null,
|
||||
number_children nonnegative_integer not null,
|
||||
number_dogs nonnegative_integer not null,
|
||||
cost_per_night nonnegative_integer not null
|
||||
);
|
||||
|
||||
grant select on table acsi to guest;
|
||||
grant select on table acsi to employee;
|
||||
grant select, insert, update, delete on table acsi to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,21 @@
|
|||
-- Deploy camper:acsi_calendar to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: acsi
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table acsi_calendar (
|
||||
campsite_type_id integer not null references acsi,
|
||||
acsi_range daterange not null,
|
||||
primary key (campsite_type_id, acsi_range),
|
||||
constraint disallow_acsi_overlap exclude using gist (campsite_type_id with =, acsi_range with &&)
|
||||
);
|
||||
|
||||
grant select on table acsi_calendar to guest;
|
||||
grant select on table acsi_calendar to employee;
|
||||
grant select, insert, update, delete on table acsi_calendar to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:acsi_option to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: acsi
|
||||
-- requires: campsite_type_option
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table acsi_option (
|
||||
campsite_type_id integer not null references acsi,
|
||||
campsite_type_option_id integer not null references campsite_type_option,
|
||||
units positive_integer not null,
|
||||
primary key (campsite_type_id, campsite_type_option_id)
|
||||
);
|
||||
|
||||
grant select on table acsi_option to guest;
|
||||
grant select on table acsi_option to employee;
|
||||
grant select, insert, update, delete on table acsi_option to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,21 @@
|
|||
-- Deploy camper:acsi_option__option_group to pg
|
||||
-- requires: acsi_option
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
alter table acsi_option
|
||||
add column option_group integer not null default 0
|
||||
;
|
||||
|
||||
alter table acsi_option
|
||||
drop constraint if exists acsi_option_pkey
|
||||
;
|
||||
|
||||
alter table acsi_option
|
||||
add primary key (campsite_type_id, campsite_type_option_id, option_group)
|
||||
, alter column option_group drop default
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,108 @@
|
|||
-- Deploy camper:add_booking_from_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking__payment_fields
|
||||
-- requires: booking__stay
|
||||
-- requires: booking_option
|
||||
-- requires: payment
|
||||
-- requires: payment__acsi_card
|
||||
-- requires: payment_customer
|
||||
-- requires: payment_option
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function add_booking_from_payment(payment_slug uuid) returns integer as
|
||||
$$
|
||||
declare
|
||||
bid integer;
|
||||
begin
|
||||
insert into booking
|
||||
( company_id
|
||||
, campsite_type_id
|
||||
, stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, lang_tag
|
||||
)
|
||||
select company_id
|
||||
, campsite_type_id
|
||||
, daterange(arrival_date, departure_date)
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
, coalesce(full_name, 'Unknown')
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, coalesce(lang_tag, 'und')
|
||||
from payment
|
||||
left join payment_customer using (payment_id)
|
||||
where payment.slug = payment_slug
|
||||
returning booking_id into bid;
|
||||
|
||||
if bid is null then
|
||||
raise invalid_parameter_value using message = payment_slug || ' is not a valid payment.';
|
||||
end if;
|
||||
|
||||
insert into booking_option
|
||||
( booking_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select bid
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
from payment_option
|
||||
join payment using (payment_id)
|
||||
where payment.slug = payment_slug
|
||||
;
|
||||
|
||||
return bid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_booking_from_payment(uuid) from public;
|
||||
grant execute on function add_booking_from_payment(uuid) to employee;
|
||||
grant execute on function add_booking_from_payment(uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,49 @@
|
|||
-- Deploy camper:add_contact to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: country_code
|
||||
-- requires: contact
|
||||
-- requires: contact_phone
|
||||
-- requires: contact_email
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function add_contact(company_id integer, name text, id_document_type_id text, id_document_number text, phone text, email text, address text, city text, province text, postal_code text, country_code country_code) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
cslug uuid;
|
||||
begin
|
||||
insert into contact (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
|
||||
values (company_id, name, id_document_type_id, id_document_number, address, city, province, postal_code, country_code)
|
||||
returning contact_id, slug
|
||||
into cid, cslug
|
||||
;
|
||||
|
||||
if phone is not null and trim(phone) <> '' then
|
||||
insert into contact_phone (contact_id, phone)
|
||||
values (cid, parse_packed_phone_number(add_contact.phone, coalesce(country_code, 'ES')))
|
||||
;
|
||||
end if;
|
||||
|
||||
if email is not null and trim(email) <> '' then
|
||||
insert into contact_email (contact_id, email)
|
||||
values (cid, add_contact.email)
|
||||
;
|
||||
end if;
|
||||
|
||||
return cslug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) from public;
|
||||
grant execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) to employee;
|
||||
grant execute on function add_contact(integer, text, text, text, text, text, text, text, text, text, country_code) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,75 @@
|
|||
-- Deploy camper:add_invoice to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: new_invoice_product
|
||||
-- requires: tax
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_product
|
||||
-- requires: invoice_product_tax
|
||||
-- requires: next_invoice_number
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function add_invoice(company integer, invoice_date date, contact_id integer, notes text, payment_method_id integer, products new_invoice_product[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
iid integer;
|
||||
pslug uuid;
|
||||
product new_invoice_product;
|
||||
ccode text;
|
||||
ipid integer;
|
||||
begin
|
||||
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code, payment_method_id)
|
||||
select company_id
|
||||
, next_invoice_number(add_invoice.company, invoice_date)
|
||||
, invoice_date
|
||||
, contact_id
|
||||
, notes
|
||||
, currency_code
|
||||
, add_invoice.payment_method_id
|
||||
from company
|
||||
where company.company_id = add_invoice.company
|
||||
returning invoice_id, slug, currency_code
|
||||
into iid, pslug, ccode;
|
||||
|
||||
foreach product in array products
|
||||
loop
|
||||
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
|
||||
select iid
|
||||
, product.name
|
||||
, coalesce(product.description, '')
|
||||
, parse_price(product.price, currency.decimal_digits)
|
||||
, product.quantity
|
||||
, product.discount_rate
|
||||
from currency
|
||||
where currency_code = ccode
|
||||
returning invoice_product_id
|
||||
into ipid;
|
||||
|
||||
if product.product_id is not null then
|
||||
insert into invoice_product_product (invoice_product_id, product_id)
|
||||
values (ipid, product.product_id);
|
||||
end if;
|
||||
|
||||
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
|
||||
select ipid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
return pslug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) from public;
|
||||
grant execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) to employee;
|
||||
grant execute on function add_invoice(integer, date, integer, text, integer, new_invoice_product[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,39 @@
|
|||
-- Deploy camper:available_id_document_types to pg
|
||||
-- requires: id_document_type
|
||||
-- requires: id_document_type_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper;
|
||||
|
||||
insert into id_document_type (id_document_type_id, name)
|
||||
values ('D', 'DNI')
|
||||
, ('P', 'Passport')
|
||||
, ('C', 'Driving license')
|
||||
, ('I', 'Identification document')
|
||||
, ('N', 'Spanish residence permit')
|
||||
, ('X', 'Residence permit from another Member State of the European Union')
|
||||
;
|
||||
|
||||
insert into id_document_type_i18n (id_document_type_id, lang_tag, name)
|
||||
values ('D', 'ca', 'DNI')
|
||||
, ('P', 'ca', 'Passaport')
|
||||
, ('C', 'ca', 'Permís de conduir')
|
||||
, ('I', 'ca', 'Carta o document d’identitat')
|
||||
, ('N', 'ca', 'Permís de residència espanyol')
|
||||
, ('X', 'ca', 'Permís de residència d’un altre estat membre de la Unió Europea')
|
||||
, ('D', 'es', 'DNI')
|
||||
, ('P', 'es', 'Pasaporte')
|
||||
, ('C', 'es', 'Permiso de conducir')
|
||||
, ('I', 'es', 'Carta o documento de identidad')
|
||||
, ('N', 'es', 'Permiso de residencia español')
|
||||
, ('X', 'es', 'Permiso de residencia de otro Estado Miembro de la Unión Europea')
|
||||
, ('D', 'fr', 'DNI')
|
||||
, ('P', 'fr', 'Passeport')
|
||||
, ('C', 'fr', 'Permis de conduire')
|
||||
, ('I', 'fr', 'Carte d’identité')
|
||||
, ('N', 'fr', 'Permis de séjour espagnol')
|
||||
, ('X', 'fr', 'Titre de séjour d’un autre État membre de l’Union européenne')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,28 @@
|
|||
-- Deploy camper:available_invoice_status to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_status
|
||||
-- requires: invoice_status_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper;
|
||||
|
||||
insert into invoice_status (invoice_status, name)
|
||||
values ('created', 'Created')
|
||||
, ('sent', 'Sent')
|
||||
, ('paid', 'Paid')
|
||||
, ('unpaid', 'Unpaid')
|
||||
;
|
||||
|
||||
insert into invoice_status_i18n (invoice_status, lang_tag, name)
|
||||
values ('created', 'ca', 'Creada')
|
||||
, ('sent', 'ca', 'Enviada')
|
||||
, ('paid', 'ca', 'Cobrada')
|
||||
, ('unpaid', 'ca', 'No cobrada')
|
||||
, ('created', 'es', 'Creada')
|
||||
, ('sent', 'es', 'Enviada')
|
||||
, ('paid', 'es', 'Cobrada')
|
||||
, ('unpaid', 'es', 'No cobrada')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:available_sexes to pg
|
||||
-- requires: sex
|
||||
-- requires: sex_i18n
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper;
|
||||
|
||||
insert into sex (sex_id, name)
|
||||
values ('F', 'Female')
|
||||
, ('M', 'Male')
|
||||
;
|
||||
|
||||
insert into sex_i18n (sex_id, lang_tag, name)
|
||||
values ('F', 'ca', 'Femení')
|
||||
, ('M', 'ca', 'Masculí')
|
||||
, ('F', 'es', 'Femenino')
|
||||
, ('M', 'es', 'Masculino')
|
||||
, ('F', 'fr', 'Féminin')
|
||||
, ('M', 'fr', 'Masculin')
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,48 @@
|
|||
-- Deploy camper:booking__payment_fields to pg
|
||||
-- requires: booking
|
||||
-- requires: positive_integer
|
||||
-- requires: nonnegative_integer
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
alter table booking
|
||||
add column address text
|
||||
, add column postal_code text
|
||||
, add column city text
|
||||
, add column country_code country_code references country
|
||||
, add column email email
|
||||
, add column phone packed_phone_number
|
||||
, add lang_tag text not null default 'und' references language
|
||||
, add zone_preferences text not null default ''
|
||||
, add subtotal_nights nonnegative_integer not null default 0
|
||||
, add number_adults positive_integer not null default 1
|
||||
, add subtotal_adults nonnegative_integer not null default 0
|
||||
, add number_teenagers nonnegative_integer not null default 0
|
||||
, add subtotal_teenagers nonnegative_integer not null default 0
|
||||
, add number_children nonnegative_integer not null default 0
|
||||
, add subtotal_children nonnegative_integer not null default 0
|
||||
, alter column number_dogs type nonnegative_integer
|
||||
, add subtotal_dogs nonnegative_integer not null default 0
|
||||
, add subtotal_tourist_tax nonnegative_integer not null default 0
|
||||
, add total nonnegative_integer not null default 0
|
||||
, add currency_code currency_code not null default 'EUR' references currency
|
||||
;
|
||||
|
||||
alter table booking
|
||||
alter column zone_preferences drop default
|
||||
, alter column subtotal_nights drop default
|
||||
, alter column number_adults drop default
|
||||
, alter column subtotal_adults drop default
|
||||
, alter column number_teenagers drop default
|
||||
, alter column subtotal_teenagers drop default
|
||||
, alter column number_children drop default
|
||||
, alter column subtotal_children drop default
|
||||
, alter column subtotal_dogs drop default
|
||||
, alter column subtotal_tourist_tax drop default
|
||||
, alter column total drop default
|
||||
, alter column currency_code drop default
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,24 @@
|
|||
-- Deploy camper:booking__stay to pg
|
||||
-- requires: booking
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
alter table booking
|
||||
add column stay daterange constraint stay_not_empty check (not isempty(stay))
|
||||
;
|
||||
|
||||
update booking
|
||||
set stay = daterange(arrival_date, departure_date)
|
||||
;
|
||||
|
||||
alter table booking
|
||||
drop column if exists arrival_date
|
||||
, drop column if exists departure_date
|
||||
, alter column stay set not null
|
||||
;
|
||||
|
||||
create index stay_idx on booking using gist (stay);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,25 @@
|
|||
-- Deploy camper:booking_campsite to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: campsite
|
||||
-- requires: extension_btree_gist
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table booking_campsite (
|
||||
booking_id integer not null references booking,
|
||||
campsite_id integer not null references campsite,
|
||||
stay daterange not null,
|
||||
primary key (booking_id, campsite_id, stay),
|
||||
exclude using gist (campsite_id with =, stay with &&)
|
||||
);
|
||||
|
||||
create index booking_campsite_stay_idx on booking_campsite using gist (stay);
|
||||
|
||||
grant select, insert, update, delete on table booking_campsite to employee;
|
||||
grant select, insert, update, delete on table booking_campsite to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,35 @@
|
|||
-- Deploy camper:booking_guest to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: sex
|
||||
-- requires: id_document_type
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table booking_guest (
|
||||
booking_guest_id integer generated by default as identity primary key,
|
||||
booking_id integer not null references booking,
|
||||
id_document_type_id varchar(1) not null references id_document_type,
|
||||
id_document_number text not null,
|
||||
id_document_issue_date date,
|
||||
given_name text not null,
|
||||
first_surname text not null,
|
||||
second_surname text not null,
|
||||
sex_id varchar(1) not null references sex,
|
||||
birthdate date not null,
|
||||
country_code country_code not null references country,
|
||||
phone packed_phone_number,
|
||||
address text not null,
|
||||
created_at timestamp with time zone not null default current_timestamp,
|
||||
updated_at timestamp with time zone not null default current_timestamp,
|
||||
unique (booking_id, id_document_type_id, id_document_number)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table booking_guest to employee;
|
||||
grant select, insert, update, delete on table booking_guest to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,20 @@
|
|||
-- Deploy camper:booking_invoice to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: invoice
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table booking_invoice (
|
||||
booking_id integer not null references booking,
|
||||
invoice_id integer not null references invoice,
|
||||
primary key (booking_id, invoice_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table booking_invoice to employee;
|
||||
grant select, insert, update, delete on table booking_invoice to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,24 @@
|
|||
-- Deploy camper:booking_option to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: campsite_type_option
|
||||
-- requires: positive_integer
|
||||
-- requires: nonnegative_integer
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table booking_option (
|
||||
booking_id integer not null references booking,
|
||||
campsite_type_option_id integer not null references campsite_type_option,
|
||||
units positive_integer not null,
|
||||
subtotal nonnegative_integer not null,
|
||||
primary key (booking_id, campsite_type_option_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table booking_option to employee;
|
||||
grant select, insert, update, delete on table booking_option to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,12 @@
|
|||
-- Deploy camper:campsite_type__operating_dates to pg
|
||||
-- requires: campsite_type
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
alter table campsite_type
|
||||
add column operating_dates daterange not null default 'empty'
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:cancel_booking to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking_campsite
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function cancel_booking(bid integer) returns void as
|
||||
$$
|
||||
delete from booking_campsite where booking_id = bid;
|
||||
update booking set booking_status = 'cancelled' where booking_id = bid;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function cancel_booking(integer) from public;
|
||||
grant execute on function cancel_booking(integer) to employee;
|
||||
grant execute on function cancel_booking(integer) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,75 @@
|
|||
-- Deploy camper:check_in_guests to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking_guest
|
||||
-- requires: checked_in_guest
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function check_in_guests(bid integer, guests checked_in_guest[]) returns void as
|
||||
$$
|
||||
insert into booking_guest
|
||||
( booking_id
|
||||
, id_document_type_id
|
||||
, id_document_number
|
||||
, id_document_issue_date
|
||||
, given_name
|
||||
, first_surname
|
||||
, second_surname
|
||||
, sex_id
|
||||
, birthdate
|
||||
, country_code
|
||||
, phone
|
||||
, address
|
||||
)
|
||||
select bid
|
||||
, id_document_type_id
|
||||
, id_document_number
|
||||
, id_document_issue_date
|
||||
, given_name
|
||||
, first_surname
|
||||
, second_surname
|
||||
, sex_id
|
||||
, birthdate
|
||||
, country_code
|
||||
, case when phone is null or phone = '' then null else parse_packed_phone_number(phone, country_code) end
|
||||
, address
|
||||
from unnest(guests) as guest
|
||||
on conflict (booking_id, id_document_type_id, id_document_number) do update
|
||||
set id_document_type_id = excluded.id_document_type_id
|
||||
, id_document_number = excluded.id_document_number
|
||||
, id_document_issue_date = excluded.id_document_issue_date
|
||||
, given_name = excluded.given_name
|
||||
, first_surname = excluded.first_surname
|
||||
, second_surname = excluded.second_surname
|
||||
, sex_id = excluded.sex_id
|
||||
, birthdate = excluded.birthdate
|
||||
, country_code = excluded.country_code
|
||||
, phone = excluded.phone
|
||||
, address = excluded.address
|
||||
, updated_at = current_timestamp
|
||||
;
|
||||
|
||||
delete from booking_guest
|
||||
where booking_id = bid
|
||||
and updated_at < current_timestamp
|
||||
;
|
||||
|
||||
update booking
|
||||
set booking_status = 'checked-in'
|
||||
where booking_id = bid
|
||||
and booking_status in ('created', 'confirmed')
|
||||
;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function check_in_guests(integer, checked_in_guest[]) from public;
|
||||
grant execute on function check_in_guests(integer, checked_in_guest[]) to employee;
|
||||
grant execute on function check_in_guests(integer, checked_in_guest[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:checked_in_guest to pg
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create type checked_in_guest as (
|
||||
id_document_type_id varchar(1),
|
||||
id_document_number text,
|
||||
id_document_issue_date date,
|
||||
given_name text,
|
||||
first_surname text,
|
||||
second_surname text,
|
||||
sex_id varchar(1),
|
||||
birthdate date,
|
||||
country_code country_code,
|
||||
phone text,
|
||||
address text
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,64 @@
|
|||
-- Deploy camper:compute_new_invoice_amount to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: company
|
||||
-- requires: currency
|
||||
-- requires: tax
|
||||
-- requires: new_invoice_product
|
||||
-- requires: new_invoice_amount
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function compute_new_invoice_amount(company_id integer, products new_invoice_product[]) returns new_invoice_amount as
|
||||
$$
|
||||
declare
|
||||
result new_invoice_amount;
|
||||
begin
|
||||
if array_length(products, 1) is null then
|
||||
select to_price(0, decimal_digits), array[]::text[][], to_price(0, decimal_digits)
|
||||
from company
|
||||
join currency using (currency_code)
|
||||
where company.company_id = compute_new_invoice_amount.company_id
|
||||
into result.subtotal, result.taxes, result.total;
|
||||
else
|
||||
with product as (
|
||||
select round(parse_price(price, currency.decimal_digits) * quantity * (1 - discount_rate))::integer as subtotal
|
||||
, tax
|
||||
, decimal_digits
|
||||
from unnest(products)
|
||||
join company on company.company_id = compute_new_invoice_amount.company_id
|
||||
join currency using (currency_code)
|
||||
)
|
||||
, tax_amount as (
|
||||
select tax_id
|
||||
, sum(round(subtotal * tax.rate)::integer)::integer as amount
|
||||
, decimal_digits
|
||||
from product, unnest(product.tax) as product_tax(tax_id)
|
||||
join tax using (tax_id)
|
||||
group by tax_id, decimal_digits
|
||||
)
|
||||
, tax_total as (
|
||||
select sum(amount)::integer as amount, array_agg(array[name, to_price(amount, decimal_digits)]) as taxes
|
||||
from tax_amount
|
||||
join tax using (tax_id)
|
||||
)
|
||||
select to_price(sum(subtotal)::integer, decimal_digits)
|
||||
, coalesce(taxes, array[]::text[][])
|
||||
, to_price(sum(subtotal)::integer + coalesce(tax_total.amount, 0), decimal_digits) as total
|
||||
from product, tax_total
|
||||
group by tax_total.amount, taxes, decimal_digits
|
||||
into result.subtotal, result.taxes, result.total;
|
||||
end if;
|
||||
|
||||
return result;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
stable;
|
||||
|
||||
revoke execute on function compute_new_invoice_amount(integer, new_invoice_product[]) from public;
|
||||
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to employee;
|
||||
grant execute on function compute_new_invoice_amount(integer, new_invoice_product[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,45 @@
|
|||
-- Deploy camper:contact to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
-- requires: id_document_type
|
||||
-- requires: country_code
|
||||
-- requires: country
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table contact (
|
||||
contact_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 1),
|
||||
id_document_type_id varchar(1) not null references id_document_type,
|
||||
id_document_number text not null,
|
||||
address text not null,
|
||||
city text not null,
|
||||
province text not null,
|
||||
postal_code text not null,
|
||||
country_code country_code not null references country,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact to employee;
|
||||
grant select, insert, update, delete on table contact to admin;
|
||||
|
||||
alter table contact enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = contact.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,31 @@
|
|||
-- Deploy camper:contact_email to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: email
|
||||
-- requires: contact
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table contact_email (
|
||||
contact_id integer primary key references contact,
|
||||
email email not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_email to employee;
|
||||
grant select, insert, update, delete on table contact_email to admin;
|
||||
|
||||
alter table contact_email enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_email
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_email.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,30 @@
|
|||
-- Deploy camper:contact_phone to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table contact_phone (
|
||||
contact_id integer primary key references contact,
|
||||
phone packed_phone_number not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table contact_phone to employee;
|
||||
grant select, insert, update, delete on table contact_phone to admin;
|
||||
|
||||
alter table contact_phone enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on contact_phone
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from contact
|
||||
where contact.contact_id = contact_phone.contact_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,14 @@
|
|||
-- Deploy camper:discount_rate to pg
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create domain discount_rate as numeric
|
||||
check (VALUE >= 0 and VALUE <= 1);
|
||||
|
||||
comment on domain discount_rate is
|
||||
'A rate for discount in the range [0, 1]';
|
||||
|
||||
commit;
|
|
@ -11,12 +11,16 @@
|
|||
-- requires: payment
|
||||
-- requires: payment_option
|
||||
-- requires: company__tourist_tax_max_days
|
||||
-- requires: acsi
|
||||
-- requires: acsi_calendar
|
||||
-- requires: acsi_option
|
||||
-- requires: acsi_option__option_group
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, options option_units[]) returns payment as
|
||||
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, acsi_card boolean, options option_units[]) returns payment as
|
||||
$$
|
||||
declare
|
||||
p payment;
|
||||
|
@ -44,26 +48,28 @@ begin
|
|||
, currency_code
|
||||
, down_payment_percent
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
)
|
||||
select coalesce(payment_slug, gen_random_uuid())
|
||||
, company_id
|
||||
, campsite_type_id
|
||||
, campsite_type.campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, sum(cost.cost_per_night * ceiling((num_adults::numeric + num_teenagers::numeric + num_children::numeric) / max_campers::numeric)::integer)::integer
|
||||
, sum(coalesce(acsi.cost_per_night, 0) + cost.cost_per_night * (ceiling((num_adults::numeric + num_teenagers::numeric + num_children::numeric) / max_campers::numeric)::integer - case when acsi.cost_per_night is null then 0 else 1 end))::integer
|
||||
, num_adults
|
||||
, sum(cost_per_adult * num_adults)::integer
|
||||
, sum(cost_per_adult * greatest(0, num_adults - coalesce(acsi.number_adults, 0)))::integer
|
||||
, num_teenagers
|
||||
, sum(cost_per_teenager * num_teenagers)::integer
|
||||
, sum(cost_per_teenager * greatest(0, num_teenagers - coalesce(acsi.number_teenagers, 0)))::integer
|
||||
, num_children
|
||||
, sum(cost_per_child * num_children)::integer
|
||||
, sum(cost_per_child * greatest(0, num_children - coalesce(acsi.number_children, 0)))::integer
|
||||
, num_dogs
|
||||
, sum(case when num_dogs > 0 then coalesce(pet.cost_per_night, 0) else 0 end)::integer
|
||||
, sum(coalesce(pet.cost_per_night, 0) * greatest(0, num_dogs - coalesce(acsi.number_dogs, 0)))::integer
|
||||
, sum(case when day_num <= tourist_tax_max_days then tourist_tax * num_adults else 0 end)::integer
|
||||
, 0
|
||||
, currency_code
|
||||
, case when arrival_date - current_date >= 7 then 0.3 else 1.0 end
|
||||
, coalesce(zone_preferences, '')
|
||||
, acsi_card
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') with ordinality as date(day, day_num)
|
||||
left join season_calendar on season_range @> date.day::date
|
||||
left join season using (season_id)
|
||||
|
@ -71,9 +77,13 @@ begin
|
|||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join campsite_type_cost as cost using (campsite_type_id, season_id)
|
||||
left join company using (company_id)
|
||||
left join (acsi join acsi_calendar using (campsite_type_id)) as acsi
|
||||
on acsi_card
|
||||
and acsi.campsite_type_id = campsite_type.campsite_type_id
|
||||
and date.day::date <@ acsi_range
|
||||
where campsite_type.slug = campsite_type_slug
|
||||
group by company_id
|
||||
, campsite_type_id
|
||||
, campsite_type.campsite_type_id
|
||||
, currency_code
|
||||
on conflict (slug) do update
|
||||
set company_id = excluded.company_id
|
||||
|
@ -94,6 +104,7 @@ begin
|
|||
, currency_code = excluded.currency_code
|
||||
, down_payment_percent = excluded.down_payment_percent
|
||||
, zone_preferences = excluded.zone_preferences
|
||||
, acsi_card = excluded.acsi_card
|
||||
, updated_at = current_timestamp
|
||||
returning *
|
||||
into p
|
||||
|
@ -108,6 +119,26 @@ begin
|
|||
from unnest(options) as option(campsite_type_option_id, units)
|
||||
);
|
||||
|
||||
with discountable_acsi_option as (
|
||||
select distinct day, campsite_type_option_id, units
|
||||
from (
|
||||
select day, campsite_type_option_id, units, row_number() over (partition by day, option_group order by cost desc) as rn
|
||||
from (
|
||||
select day, campsite_type_option_id, units, cost, min(option_group) option_group from (
|
||||
select day, campsite_type_option_id, acsi_option.units, cost, option_group, count(*) over (partition by day, option_group) > 1 as already_used
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
join acsi_calendar on acsi_card and day::date <@ acsi_range
|
||||
join acsi_option as acsi_option using (campsite_type_option_id)
|
||||
) as with_count
|
||||
group by day, campsite_type_option_id, units, cost, already_used
|
||||
) as by_group
|
||||
) as discountable
|
||||
where acsi_card
|
||||
and rn = 1
|
||||
)
|
||||
insert into payment_option (
|
||||
payment_id
|
||||
, campsite_type_option_id
|
||||
|
@ -115,16 +146,17 @@ begin
|
|||
, subtotal
|
||||
)
|
||||
select p.payment_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, case when per_night then sum(cost * units)::integer else max(cost * units)::integer end
|
||||
, campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, case when per_night then sum(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer else max(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer end
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join campsite_type_option using (campsite_type_option_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
group by campsite_type_option_id
|
||||
, units
|
||||
left join discountable_acsi_option as acsi_option using (day, campsite_type_option_id)
|
||||
group by campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, per_night
|
||||
on conflict (payment_id, campsite_type_option_id) do update
|
||||
set units = excluded.units
|
||||
|
@ -161,9 +193,9 @@ $$
|
|||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) from public;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to guest;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to employee;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to admin;
|
||||
revoke execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) from public;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to guest;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to employee;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
-- Deploy camper:draft_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: season_calendar
|
||||
-- requires: season
|
||||
-- requires: campsite_type
|
||||
-- requires: campsite_type_pet_cost
|
||||
-- requires: campsite_type_cost
|
||||
-- requires: campsite_type_option_cost
|
||||
-- requires: campsite_type_option
|
||||
-- requires: payment
|
||||
-- requires: payment_option
|
||||
-- requires: company__tourist_tax_max_days
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, options option_units[]) returns payment as
|
||||
$$
|
||||
declare
|
||||
p payment;
|
||||
begin
|
||||
if exists(select 1 from payment where slug = payment_slug and payment_status <> 'draft') then
|
||||
payment_slug = null;
|
||||
end if;
|
||||
insert into payment (
|
||||
slug
|
||||
, company_id
|
||||
, campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, down_payment_percent
|
||||
, zone_preferences
|
||||
)
|
||||
select coalesce(payment_slug, gen_random_uuid())
|
||||
, company_id
|
||||
, campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, sum(cost.cost_per_night * ceiling((num_adults::numeric + num_teenagers::numeric + num_children::numeric) / max_campers::numeric)::integer)::integer
|
||||
, num_adults
|
||||
, sum(cost_per_adult * num_adults)::integer
|
||||
, num_teenagers
|
||||
, sum(cost_per_teenager * num_teenagers)::integer
|
||||
, num_children
|
||||
, sum(cost_per_child * num_children)::integer
|
||||
, num_dogs
|
||||
, sum(case when num_dogs > 0 then coalesce(pet.cost_per_night, 0) else 0 end)::integer
|
||||
, sum(case when day_num <= tourist_tax_max_days then tourist_tax * num_adults else 0 end)::integer
|
||||
, 0
|
||||
, currency_code
|
||||
, case when arrival_date - current_date >= 7 then 0.3 else 1.0 end
|
||||
, coalesce(zone_preferences, '')
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') with ordinality as date(day, day_num)
|
||||
left join season_calendar on season_range @> date.day::date
|
||||
left join season using (season_id)
|
||||
left join campsite_type using (company_id)
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join campsite_type_cost as cost using (campsite_type_id, season_id)
|
||||
left join company using (company_id)
|
||||
where campsite_type.slug = campsite_type_slug
|
||||
group by company_id
|
||||
, campsite_type_id
|
||||
, currency_code
|
||||
on conflict (slug) do update
|
||||
set company_id = excluded.company_id
|
||||
, campsite_type_id = excluded.campsite_type_id
|
||||
, arrival_date = excluded.arrival_date
|
||||
, departure_date = excluded.departure_date
|
||||
, subtotal_nights = excluded.subtotal_nights
|
||||
, number_adults = excluded.number_adults
|
||||
, subtotal_adults = excluded.subtotal_adults
|
||||
, number_teenagers = excluded.number_teenagers
|
||||
, subtotal_teenagers = excluded.subtotal_teenagers
|
||||
, number_children = excluded.number_children
|
||||
, subtotal_children = excluded.subtotal_children
|
||||
, number_dogs = excluded.number_dogs
|
||||
, subtotal_dogs = excluded.subtotal_dogs
|
||||
, subtotal_tourist_tax = excluded.subtotal_tourist_tax
|
||||
, total = excluded.total
|
||||
, currency_code = excluded.currency_code
|
||||
, down_payment_percent = excluded.down_payment_percent
|
||||
, zone_preferences = excluded.zone_preferences
|
||||
, updated_at = current_timestamp
|
||||
returning *
|
||||
into p
|
||||
;
|
||||
|
||||
if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
and campsite_type_option_id not in (
|
||||
select campsite_type_option_id
|
||||
from unnest(options) as option(campsite_type_option_id, units)
|
||||
);
|
||||
|
||||
insert into payment_option (
|
||||
payment_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select p.payment_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, case when per_night then sum(cost * units)::integer else max(cost * units)::integer end
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join campsite_type_option using (campsite_type_option_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
group by campsite_type_option_id
|
||||
, units
|
||||
, per_night
|
||||
on conflict (payment_id, campsite_type_option_id) do update
|
||||
set units = excluded.units
|
||||
, subtotal = excluded.subtotal
|
||||
;
|
||||
|
||||
with option as (
|
||||
select sum(subtotal)::integer as subtotal
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
)
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0)
|
||||
from option
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
else
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id;
|
||||
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
end if;
|
||||
|
||||
|
||||
return p;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) from public;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to guest;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to employee;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,183 @@
|
|||
-- Deploy camper:draft_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: season_calendar
|
||||
-- requires: season
|
||||
-- requires: campsite_type
|
||||
-- requires: campsite_type_pet_cost
|
||||
-- requires: campsite_type_cost
|
||||
-- requires: campsite_type_option_cost
|
||||
-- requires: campsite_type_option
|
||||
-- requires: payment
|
||||
-- requires: payment_option
|
||||
-- requires: company__tourist_tax_max_days
|
||||
-- requires: acsi
|
||||
-- requires: acsi_calendar
|
||||
-- requires: acsi_options
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
drop function if exists draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, option_units[]);
|
||||
|
||||
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, acsi_card boolean, options option_units[]) returns payment as
|
||||
$$
|
||||
declare
|
||||
p payment;
|
||||
begin
|
||||
if exists(select 1 from payment where slug = payment_slug and payment_status <> 'draft') then
|
||||
payment_slug = null;
|
||||
end if;
|
||||
insert into payment (
|
||||
slug
|
||||
, company_id
|
||||
, campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, down_payment_percent
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
)
|
||||
select coalesce(payment_slug, gen_random_uuid())
|
||||
, company_id
|
||||
, campsite_type.campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, sum(coalesce(acsi.cost_per_night, 0) + cost.cost_per_night * (ceiling((num_adults::numeric + num_teenagers::numeric + num_children::numeric) / max_campers::numeric)::integer - case when acsi.cost_per_night is null then 0 else 1 end))::integer
|
||||
, num_adults
|
||||
, sum(cost_per_adult * greatest(0, num_adults - coalesce(acsi.number_adults, 0)))::integer
|
||||
, num_teenagers
|
||||
, sum(cost_per_teenager * greatest(0, num_teenagers - coalesce(acsi.number_teenagers, 0)))::integer
|
||||
, num_children
|
||||
, sum(cost_per_child * greatest(0, num_children - coalesce(acsi.number_children, 0)))::integer
|
||||
, num_dogs
|
||||
, sum(case when (num_dogs - coalesce(acsi.number_dogs, 0)) > 0 then coalesce(pet.cost_per_night, 0) else 0 end)::integer
|
||||
, sum(case when day_num <= tourist_tax_max_days then tourist_tax * num_adults else 0 end)::integer
|
||||
, 0
|
||||
, currency_code
|
||||
, case when arrival_date - current_date >= 7 then 0.3 else 1.0 end
|
||||
, coalesce(zone_preferences, '')
|
||||
, acsi_card
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') with ordinality as date(day, day_num)
|
||||
left join season_calendar on season_range @> date.day::date
|
||||
left join season using (season_id)
|
||||
left join campsite_type using (company_id)
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join campsite_type_cost as cost using (campsite_type_id, season_id)
|
||||
left join company using (company_id)
|
||||
left join (acsi join acsi_calendar using (campsite_type_id)) as acsi
|
||||
on acsi_card
|
||||
and acsi.campsite_type_id = campsite_type.campsite_type_id
|
||||
and date.day::date <@ acsi_range
|
||||
where campsite_type.slug = campsite_type_slug
|
||||
group by company_id
|
||||
, campsite_type.campsite_type_id
|
||||
, currency_code
|
||||
on conflict (slug) do update
|
||||
set company_id = excluded.company_id
|
||||
, campsite_type_id = excluded.campsite_type_id
|
||||
, arrival_date = excluded.arrival_date
|
||||
, departure_date = excluded.departure_date
|
||||
, subtotal_nights = excluded.subtotal_nights
|
||||
, number_adults = excluded.number_adults
|
||||
, subtotal_adults = excluded.subtotal_adults
|
||||
, number_teenagers = excluded.number_teenagers
|
||||
, subtotal_teenagers = excluded.subtotal_teenagers
|
||||
, number_children = excluded.number_children
|
||||
, subtotal_children = excluded.subtotal_children
|
||||
, number_dogs = excluded.number_dogs
|
||||
, subtotal_dogs = excluded.subtotal_dogs
|
||||
, subtotal_tourist_tax = excluded.subtotal_tourist_tax
|
||||
, total = excluded.total
|
||||
, currency_code = excluded.currency_code
|
||||
, down_payment_percent = excluded.down_payment_percent
|
||||
, zone_preferences = excluded.zone_preferences
|
||||
, acsi_card = excluded.acsi_card
|
||||
, updated_at = current_timestamp
|
||||
returning *
|
||||
into p
|
||||
;
|
||||
|
||||
if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
and campsite_type_option_id not in (
|
||||
select campsite_type_option_id
|
||||
from unnest(options) as option(campsite_type_option_id, units)
|
||||
);
|
||||
|
||||
insert into payment_option (
|
||||
payment_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select p.payment_id
|
||||
, campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, case when per_night then sum(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer else max(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer end
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join campsite_type_option using (campsite_type_option_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
left join acsi_calendar on acsi_card and day::date <@ acsi_range
|
||||
left join acsi_option on acsi_option.campsite_type_id = acsi_calendar.campsite_type_id and acsi_option.campsite_type_option_id = campsite_type_option.campsite_type_option_id
|
||||
group by campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, per_night
|
||||
on conflict (payment_id, campsite_type_option_id) do update
|
||||
set units = excluded.units
|
||||
, subtotal = excluded.subtotal
|
||||
;
|
||||
|
||||
with option as (
|
||||
select sum(subtotal)::integer as subtotal
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
)
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0)
|
||||
from option
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
else
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id;
|
||||
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
end if;
|
||||
|
||||
|
||||
return p;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) from public;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to guest;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to employee;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,194 @@
|
|||
-- Deploy camper:draft_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: season_calendar
|
||||
-- requires: season
|
||||
-- requires: campsite_type
|
||||
-- requires: campsite_type_pet_cost
|
||||
-- requires: campsite_type_cost
|
||||
-- requires: campsite_type_option_cost
|
||||
-- requires: campsite_type_option
|
||||
-- requires: payment
|
||||
-- requires: payment_option
|
||||
-- requires: company__tourist_tax_max_days
|
||||
-- requires: acsi
|
||||
-- requires: acsi_calendar
|
||||
-- requires: acsi_options
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, acsi_card boolean, options option_units[]) returns payment as
|
||||
$$
|
||||
declare
|
||||
p payment;
|
||||
begin
|
||||
if exists(select 1 from payment where slug = payment_slug and payment_status <> 'draft') then
|
||||
payment_slug = null;
|
||||
end if;
|
||||
insert into payment (
|
||||
slug
|
||||
, company_id
|
||||
, campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, down_payment_percent
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
)
|
||||
select coalesce(payment_slug, gen_random_uuid())
|
||||
, company_id
|
||||
, campsite_type.campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, sum(coalesce(acsi.cost_per_night, 0) + cost.cost_per_night * (ceiling((num_adults::numeric + num_teenagers::numeric + num_children::numeric) / max_campers::numeric)::integer - case when acsi.cost_per_night is null then 0 else 1 end))::integer
|
||||
, num_adults
|
||||
, sum(cost_per_adult * greatest(0, num_adults - coalesce(acsi.number_adults, 0)))::integer
|
||||
, num_teenagers
|
||||
, sum(cost_per_teenager * greatest(0, num_teenagers - coalesce(acsi.number_teenagers, 0)))::integer
|
||||
, num_children
|
||||
, sum(cost_per_child * greatest(0, num_children - coalesce(acsi.number_children, 0)))::integer
|
||||
, num_dogs
|
||||
, sum(coalesce(pet.cost_per_night, 0) * greatest(0, num_dogs - coalesce(acsi.number_dogs, 0)))::integer
|
||||
, sum(case when day_num <= tourist_tax_max_days then tourist_tax * num_adults else 0 end)::integer
|
||||
, 0
|
||||
, currency_code
|
||||
, case when arrival_date - current_date >= 7 then 0.3 else 1.0 end
|
||||
, coalesce(zone_preferences, '')
|
||||
, acsi_card
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') with ordinality as date(day, day_num)
|
||||
left join season_calendar on season_range @> date.day::date
|
||||
left join season using (season_id)
|
||||
left join campsite_type using (company_id)
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join campsite_type_cost as cost using (campsite_type_id, season_id)
|
||||
left join company using (company_id)
|
||||
left join (acsi join acsi_calendar using (campsite_type_id)) as acsi
|
||||
on acsi_card
|
||||
and acsi.campsite_type_id = campsite_type.campsite_type_id
|
||||
and date.day::date <@ acsi_range
|
||||
where campsite_type.slug = campsite_type_slug
|
||||
group by company_id
|
||||
, campsite_type.campsite_type_id
|
||||
, currency_code
|
||||
on conflict (slug) do update
|
||||
set company_id = excluded.company_id
|
||||
, campsite_type_id = excluded.campsite_type_id
|
||||
, arrival_date = excluded.arrival_date
|
||||
, departure_date = excluded.departure_date
|
||||
, subtotal_nights = excluded.subtotal_nights
|
||||
, number_adults = excluded.number_adults
|
||||
, subtotal_adults = excluded.subtotal_adults
|
||||
, number_teenagers = excluded.number_teenagers
|
||||
, subtotal_teenagers = excluded.subtotal_teenagers
|
||||
, number_children = excluded.number_children
|
||||
, subtotal_children = excluded.subtotal_children
|
||||
, number_dogs = excluded.number_dogs
|
||||
, subtotal_dogs = excluded.subtotal_dogs
|
||||
, subtotal_tourist_tax = excluded.subtotal_tourist_tax
|
||||
, total = excluded.total
|
||||
, currency_code = excluded.currency_code
|
||||
, down_payment_percent = excluded.down_payment_percent
|
||||
, zone_preferences = excluded.zone_preferences
|
||||
, acsi_card = excluded.acsi_card
|
||||
, updated_at = current_timestamp
|
||||
returning *
|
||||
into p
|
||||
;
|
||||
|
||||
if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
and campsite_type_option_id not in (
|
||||
select campsite_type_option_id
|
||||
from unnest(options) as option(campsite_type_option_id, units)
|
||||
);
|
||||
|
||||
with discountable_acsi_option as (
|
||||
select day, campsite_type_option_id, units
|
||||
from (
|
||||
select day, campsite_type_option_id, acsi_option.units, row_number() over (partition by day order by cost desc) as rn
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
join acsi_calendar on acsi_card and day::date <@ acsi_range
|
||||
join acsi_option as acsi_option using (campsite_type_option_id)
|
||||
) as discountable
|
||||
where acsi_card
|
||||
and rn = 1
|
||||
)
|
||||
insert into payment_option (
|
||||
payment_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select p.payment_id
|
||||
, campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, case when per_night then sum(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer else max(cost * greatest(0, option.units - coalesce(acsi_option.units, 0)))::integer end
|
||||
from generate_series(arrival_date, departure_date - 1, interval '1 day') as date(day)
|
||||
join season_calendar on season_range @> date.day::date
|
||||
join campsite_type_option_cost using (season_id)
|
||||
join campsite_type_option using (campsite_type_option_id)
|
||||
join unnest(options) as option(campsite_type_option_id, units) using (campsite_type_option_id)
|
||||
left join discountable_acsi_option as acsi_option using (day, campsite_type_option_id)
|
||||
group by campsite_type_option.campsite_type_option_id
|
||||
, option.units
|
||||
, per_night
|
||||
on conflict (payment_id, campsite_type_option_id) do update
|
||||
set units = excluded.units
|
||||
, subtotal = excluded.subtotal
|
||||
;
|
||||
|
||||
with option as (
|
||||
select sum(subtotal)::integer as subtotal
|
||||
from payment_option
|
||||
where payment_id = p.payment_id
|
||||
)
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0)
|
||||
from option
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
else
|
||||
delete
|
||||
from payment_option
|
||||
where payment_id = p.payment_id;
|
||||
|
||||
update payment
|
||||
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax
|
||||
where payment_id = p.payment_id
|
||||
returning total into p.total
|
||||
;
|
||||
end if;
|
||||
|
||||
|
||||
return p;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) from public;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to guest;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to employee;
|
||||
grant execute on function draft_payment(uuid, date, date, uuid, integer, integer, integer, integer, text, boolean, option_units[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,48 @@
|
|||
-- Deploy camper:edit_booking to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking__payment_fields
|
||||
-- requires: booking__stay
|
||||
-- requires: booking_campsite
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_booking(bid integer, customer_name text, customer_address text, customer_post_code text, customer_city text, customer_country_code text, customer_email email, customer_phone text, customer_lang_tag text, booking_status text, campsite_ids integer[]) returns void as
|
||||
$$
|
||||
begin
|
||||
update booking
|
||||
set holder_name = customer_name
|
||||
, address = customer_address
|
||||
, postal_code = customer_post_code
|
||||
, city = customer_city
|
||||
, country_code = customer_country_code
|
||||
, email = customer_email
|
||||
, phone = case when customer_phone is null then null else parse_packed_phone_number(customer_phone, coalesce(customer_country_code, 'ES')) end
|
||||
, lang_tag = coalesce(customer_lang_tag, 'und')
|
||||
, booking_status = edit_booking.booking_status
|
||||
where booking_id = bid
|
||||
;
|
||||
|
||||
delete from booking_campsite
|
||||
where booking_id = bid;
|
||||
|
||||
insert into booking_campsite
|
||||
select bid
|
||||
, campsite_id
|
||||
, stay
|
||||
from booking, unnest(campsite_ids) as campsite(campsite_id)
|
||||
where booking_id = bid
|
||||
;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_booking(integer, text, text, text, text, text, email, text, text, text, integer[]) from public;
|
||||
grant execute on function edit_booking(integer, text, text, text, text, text, email, text, text, text, integer[]) to employee;
|
||||
grant execute on function edit_booking(integer, text, text, text, text, text, email, text, text, text, integer[]) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,92 @@
|
|||
-- Deploy camper:edit_booking_from_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking__payment_fields
|
||||
-- requires: booking__stay
|
||||
-- requires: booking_option
|
||||
-- requires: payment
|
||||
-- requires: payment__acsi_card
|
||||
-- requires: payment_option
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_booking_from_payment(booking_slug uuid, payment_slug uuid) returns integer as
|
||||
$$
|
||||
declare
|
||||
bid integer;
|
||||
begin
|
||||
with p as (
|
||||
select company_id
|
||||
, campsite_type_id
|
||||
, daterange(arrival_date, departure_date) as stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
from payment
|
||||
where payment.slug = payment_slug
|
||||
)
|
||||
update booking
|
||||
set company_id = p.company_id
|
||||
, campsite_type_id = p.campsite_type_id
|
||||
, stay = p.stay
|
||||
, subtotal_nights = p.subtotal_nights
|
||||
, number_adults = p.number_adults
|
||||
, subtotal_adults = p.subtotal_adults
|
||||
, number_teenagers = p.number_teenagers
|
||||
, subtotal_teenagers = p.subtotal_teenagers
|
||||
, number_children = p.number_children
|
||||
, subtotal_children = p.subtotal_children
|
||||
, number_dogs = p.number_dogs
|
||||
, subtotal_dogs = p.subtotal_dogs
|
||||
, subtotal_tourist_tax = p.subtotal_tourist_tax
|
||||
, total = p.total
|
||||
, currency_code = p.currency_code
|
||||
, zone_preferences = p.zone_preferences
|
||||
, acsi_card = p.acsi_card
|
||||
from p
|
||||
where slug = booking_slug
|
||||
returning booking_id into bid;
|
||||
|
||||
delete from booking_option
|
||||
where booking_id = bid;
|
||||
|
||||
insert into booking_option
|
||||
( booking_id
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
)
|
||||
select bid
|
||||
, campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
from payment_option
|
||||
join payment using (payment_id)
|
||||
where payment.slug = payment_slug
|
||||
;
|
||||
|
||||
return bid;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_booking_from_payment(uuid, uuid) from public;
|
||||
grant execute on function edit_booking_from_payment(uuid, uuid) to employee;
|
||||
grant execute on function edit_booking_from_payment(uuid, uuid) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,72 @@
|
|||
-- Deploy camper:edit_contact to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: email
|
||||
-- requires: country_code
|
||||
-- requires: contact
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: contact_phone
|
||||
-- requires: contact_email
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_contact(contact_slug uuid, name text, id_document_type_id text, id_document_number text, phone text, email text, address text, city text, province text, postal_code text, country_code country_code) returns uuid as
|
||||
$$
|
||||
declare
|
||||
cid integer;
|
||||
begin
|
||||
update contact
|
||||
set name = edit_contact.name
|
||||
, id_document_type_id = edit_contact.id_document_type_id
|
||||
, id_document_number = edit_contact.id_document_number
|
||||
, address = edit_contact.address
|
||||
, city = edit_contact.city
|
||||
, province = edit_contact.province
|
||||
, postal_code = edit_contact.postal_code
|
||||
, country_code = edit_contact.country_code
|
||||
where slug = contact_slug
|
||||
returning contact_id
|
||||
into cid
|
||||
;
|
||||
|
||||
if cid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
if phone is null or trim(phone) = '' then
|
||||
delete from contact_phone
|
||||
where contact_id = cid
|
||||
;
|
||||
else
|
||||
insert into contact_phone (contact_id, phone)
|
||||
values (cid, parse_packed_phone_number(phone, coalesce(country_code, 'ES')))
|
||||
on conflict (contact_id) do update
|
||||
set phone = excluded.phone
|
||||
;
|
||||
end if;
|
||||
|
||||
if email is null or trim(email) = '' then
|
||||
delete from contact_email
|
||||
where contact_id = cid
|
||||
;
|
||||
else
|
||||
insert into contact_email (contact_id, email)
|
||||
values (cid, email)
|
||||
on conflict (contact_id) do update
|
||||
set email = excluded.email
|
||||
;
|
||||
end if;
|
||||
|
||||
return contact_slug;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) from public;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) to employee;
|
||||
grant execute on function edit_contact(uuid, text, text, text, text, text, text, text, text, text, country_code) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,110 @@
|
|||
-- Deploy camper:edit_invoice to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice
|
||||
-- requires: currency
|
||||
-- requires: parse_price
|
||||
-- requires: edited_invoice_product
|
||||
-- requires: tax
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_product
|
||||
-- requires: invoice_product_tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, products edited_invoice_product[]) returns uuid as
|
||||
$$
|
||||
declare
|
||||
iid integer;
|
||||
products_to_keep integer[];
|
||||
products_to_delete integer[];
|
||||
company integer;
|
||||
ccode text;
|
||||
product edited_invoice_product;
|
||||
ipid integer;
|
||||
begin
|
||||
update invoice
|
||||
set contact_id = edit_invoice.contact_id
|
||||
, invoice_status = edit_invoice.invoice_status
|
||||
, notes = edit_invoice.notes
|
||||
, payment_method_id = edit_invoice.payment_method_id
|
||||
where slug = invoice_slug
|
||||
returning invoice_id, company_id, currency_code
|
||||
into iid, company, ccode
|
||||
;
|
||||
|
||||
if iid is null then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
foreach product in array products
|
||||
loop
|
||||
if product.invoice_product_id is null then
|
||||
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
|
||||
select iid
|
||||
, product.name
|
||||
, coalesce(product.description, '')
|
||||
, parse_price(product.price, currency.decimal_digits)
|
||||
, product.quantity
|
||||
, product.discount_rate
|
||||
from currency
|
||||
where currency_code = ccode
|
||||
returning invoice_product_id
|
||||
into ipid;
|
||||
else
|
||||
ipid := product.invoice_product_id;
|
||||
|
||||
update invoice_product
|
||||
set name = product.name
|
||||
, description = coalesce(product.description, '')
|
||||
, price = parse_price(product.price, currency.decimal_digits)
|
||||
, quantity = product.quantity
|
||||
, discount_rate = product.discount_rate
|
||||
from currency
|
||||
where invoice_product_id = ipid
|
||||
and currency_code = ccode;
|
||||
end if;
|
||||
products_to_keep := array_append(products_to_keep, ipid);
|
||||
|
||||
if product.product_id is null then
|
||||
delete from invoice_product_product where invoice_product_id = ipid;
|
||||
else
|
||||
insert into invoice_product_product (invoice_product_id, product_id)
|
||||
values (ipid, product.product_id)
|
||||
on conflict (invoice_product_id) do update
|
||||
set product_id = product.product_id;
|
||||
end if;
|
||||
|
||||
delete from invoice_product_tax where invoice_product_id = ipid;
|
||||
|
||||
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
|
||||
select ipid, tax_id, tax.rate
|
||||
from tax
|
||||
join unnest(product.tax) as ptax(tax_id) using (tax_id);
|
||||
end loop;
|
||||
|
||||
select array_agg(invoice_product_id)
|
||||
into products_to_delete
|
||||
from invoice_product
|
||||
where invoice_id = iid
|
||||
and not (invoice_product_id = any(products_to_keep));
|
||||
|
||||
if array_length(products_to_delete, 1) > 0 then
|
||||
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
|
||||
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
|
||||
delete from invoice_product where invoice_product_id = any(products_to_delete);
|
||||
end if;
|
||||
|
||||
return invoice_slug;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) from public;
|
||||
grant execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) to employee;
|
||||
grant execute on function edit_invoice(uuid, text, integer, text, integer, edited_invoice_product[]) to admin;
|
||||
|
||||
|
||||
commit;
|
|
@ -0,0 +1,20 @@
|
|||
-- Deploy camper:edited_invoice_product to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: discount_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create type edited_invoice_product as
|
||||
( invoice_product_id integer
|
||||
, product_id integer
|
||||
, name text
|
||||
, description text
|
||||
, price text
|
||||
, quantity integer
|
||||
, discount_rate discount_rate
|
||||
, tax integer[]
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,17 @@
|
|||
-- Deploy camper:id_document_type to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table id_document_type (
|
||||
id_document_type_id varchar(1) primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table id_document_type to employee;
|
||||
grant select on table id_document_type to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,21 @@
|
|||
-- Deploy camper:id_document_type_i18n to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: id_document_type
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table id_document_type_i18n (
|
||||
id_document_type_id varchar(1) not null references id_document_type,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (id_document_type_id, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table id_document_type_i18n to employee;
|
||||
grant select on table id_document_type_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,44 @@
|
|||
-- Deploy camper:invoice to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
-- requires: contact
|
||||
-- requires: invoice_status
|
||||
-- requires: currency
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice (
|
||||
invoice_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null unique default gen_random_uuid(),
|
||||
invoice_number text not null constraint invoice_number_not_empty check(length(trim(invoice_number)) > 1),
|
||||
invoice_date date not null default current_date,
|
||||
contact_id integer not null references contact,
|
||||
invoice_status text not null default 'created' references invoice_status,
|
||||
notes text not null default '',
|
||||
payment_method_id integer not null references payment_method,
|
||||
currency_code text not null references currency,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice to employee;
|
||||
grant select, insert, update, delete on table invoice to admin;
|
||||
|
||||
alter table invoice enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = invoice.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:invoice_amount to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_amount
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace view invoice_amount as
|
||||
select invoice_id
|
||||
, sum(subtotal)::integer as subtotal
|
||||
, sum(total)::integer as total
|
||||
from invoice_product
|
||||
join invoice_product_amount using (invoice_product_id)
|
||||
group by invoice_id
|
||||
;
|
||||
|
||||
grant select on table invoice_amount to employee;
|
||||
grant select on table invoice_amount to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,32 @@
|
|||
-- Deploy camper:invoice_number_counter to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice_number_counter (
|
||||
company_id integer not null references company,
|
||||
year integer not null constraint year_always_positive check(year > 0),
|
||||
currval integer not null constraint counter_zero_or_positive check(currval >= 0),
|
||||
primary key (company_id, year)
|
||||
);
|
||||
|
||||
grant select, insert, update on table invoice_number_counter to employee;
|
||||
grant select, insert, update on table invoice_number_counter to admin;
|
||||
|
||||
alter table invoice_number_counter enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_number_counter
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = invoice_number_counter.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,38 @@
|
|||
-- Deploy camper:invoice_product to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice
|
||||
-- requires: discount_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice_product (
|
||||
invoice_product_id integer generated by default as identity primary key,
|
||||
invoice_id integer not null references invoice,
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0),
|
||||
description text not null default '',
|
||||
price integer not null,
|
||||
quantity integer not null default 1,
|
||||
discount_rate discount_rate not null default 0.0
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_product to employee;
|
||||
grant select, insert, update, delete on table invoice_product to admin;
|
||||
|
||||
grant usage on sequence invoice_product_invoice_product_id_seq to employee;
|
||||
grant usage on sequence invoice_product_invoice_product_id_seq to admin;
|
||||
|
||||
alter table invoice_product enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_product
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from invoice
|
||||
where invoice.invoice_id = invoice_product.invoice_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:invoice_product_amount to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace view invoice_product_amount as
|
||||
select invoice_product_id
|
||||
, round(price * quantity * (1 - discount_rate))::integer as subtotal
|
||||
, max(round(price * quantity * (1 - discount_rate))::integer) + coalesce(sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer, 0) as total
|
||||
from invoice_product
|
||||
left join invoice_product_tax using (invoice_product_id)
|
||||
group by invoice_product_id, price, quantity, discount_rate
|
||||
;
|
||||
|
||||
grant select on table invoice_product_amount to employee;
|
||||
grant select on table invoice_product_amount to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,18 @@
|
|||
-- Deploy camper:invoice_product_product to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_product
|
||||
-- requires: product
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper;
|
||||
|
||||
create table invoice_product_product (
|
||||
invoice_product_id integer primary key references invoice_product,
|
||||
product_id integer not null references product
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_product_product to employee;
|
||||
grant select, insert, update, delete on table invoice_product_product to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,33 @@
|
|||
-- Deploy camper:invoice_product_tax to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_product
|
||||
-- requires: tax
|
||||
-- requires: tax_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice_product_tax (
|
||||
invoice_product_id integer not null references invoice_product,
|
||||
tax_id integer not null references tax,
|
||||
tax_rate tax_rate not null,
|
||||
primary key (invoice_product_id, tax_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table invoice_product_tax to employee;
|
||||
grant select, insert, update, delete on table invoice_product_tax to admin;
|
||||
|
||||
alter table invoice_product_tax enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on invoice_product_tax
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from invoice_product
|
||||
where invoice_product.invoice_product_id = invoice_product_tax.invoice_product_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,16 @@
|
|||
-- Deploy camper:invoice_status to pg
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice_status (
|
||||
invoice_status text primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table invoice_status to employee;
|
||||
grant select on table invoice_status to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,20 @@
|
|||
-- Deploy camper:invoice_status_i18n to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_status
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table invoice_status_i18n (
|
||||
invoice_status text not null references invoice_status,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (invoice_status, lang_tag)
|
||||
);
|
||||
|
||||
grant select on table invoice_status_i18n to employee;
|
||||
grant select on table invoice_status_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:invoice_tax_amount to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_product
|
||||
-- requires: invoice_product_tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace view invoice_tax_amount as
|
||||
select invoice_id
|
||||
, tax_id
|
||||
, sum(round(round(price * quantity * (1 - discount_rate))::integer * tax_rate)::integer)::integer as amount
|
||||
from invoice_product
|
||||
join invoice_product_tax using (invoice_product_id)
|
||||
group by invoice_id
|
||||
, tax_id
|
||||
;
|
||||
|
||||
grant select on table invoice_tax_amount to employee;
|
||||
grant select on table invoice_tax_amount to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,67 @@
|
|||
-- Deploy camper:marshal_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: payment
|
||||
-- requires: payment_customer
|
||||
-- requires: payment_option
|
||||
-- requires: payment__acsi_card
|
||||
-- requires: payment_customer__-acsi_card
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function marshal_payment(pid integer) returns jsonb as
|
||||
$$
|
||||
select to_jsonb(ctx)
|
||||
from (
|
||||
select company_id
|
||||
, campsite_type_id
|
||||
, arrival_date
|
||||
, departure_date
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
, full_name
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, lang_tag
|
||||
, (
|
||||
select array_agg(to_jsonb(o))
|
||||
from (
|
||||
select campsite_type_option_id
|
||||
, units
|
||||
, subtotal
|
||||
from payment_option
|
||||
where payment_option.payment_id = payment.payment_id
|
||||
) o
|
||||
) as options
|
||||
from payment
|
||||
join payment_customer using (payment_id)
|
||||
where payment_id = pid
|
||||
) as ctx;
|
||||
$$
|
||||
language sql
|
||||
;
|
||||
|
||||
revoke execute on function marshal_payment(integer) from public;
|
||||
grant execute on function marshal_payment(integer) to guest;
|
||||
grant execute on function marshal_payment(integer) to employee;
|
||||
grant execute on function marshal_payment(integer) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,14 @@
|
|||
-- Deploy camper:new_invoice_amount to pg
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create type new_invoice_amount as (
|
||||
subtotal text,
|
||||
taxes text[][],
|
||||
total text
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,19 @@
|
|||
-- Deploy camper:new_invoice_product to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: discount_rate
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create type new_invoice_product as (
|
||||
product_id integer,
|
||||
name text,
|
||||
description text,
|
||||
price text,
|
||||
quantity integer,
|
||||
discount_rate discount_rate,
|
||||
tax integer[]
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,38 @@
|
|||
-- Deploy camper:next_invoice_number to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: invoice_number_counter
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function next_invoice_number(company integer, invoice_date date) returns text
|
||||
as
|
||||
$$
|
||||
declare
|
||||
num integer;
|
||||
invoice_number text;
|
||||
begin
|
||||
insert into invoice_number_counter (company_id, year, currval)
|
||||
values (next_invoice_number.company, date_part('year', invoice_date), 1)
|
||||
on conflict (company_id, year) do
|
||||
update
|
||||
set currval = invoice_number_counter.currval + 1
|
||||
returning currval
|
||||
into num;
|
||||
|
||||
select to_char(invoice_date, to_char(num, 'FM' || replace(invoice_number_format, '"', '\""')))
|
||||
into invoice_number
|
||||
from company
|
||||
where company_id = next_invoice_number.company;
|
||||
|
||||
return invoice_number;
|
||||
end;
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
revoke execute on function next_invoice_number(integer, date) from public;
|
||||
grant execute on function next_invoice_number(integer, date) to employee;
|
||||
grant execute on function next_invoice_number(integer, date) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,22 @@
|
|||
-- Deploy camper:payment__acsi_card to pg
|
||||
-- requires: payment
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
alter table payment
|
||||
add column acsi_card boolean not null default false
|
||||
;
|
||||
|
||||
update payment
|
||||
set acsi_card = payment_customer.acsi_card
|
||||
from payment_customer
|
||||
where payment_customer.payment_id = payment.payment_id
|
||||
;
|
||||
|
||||
alter table payment
|
||||
alter column acsi_card drop default
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,10 @@
|
|||
-- Deploy camper:payment_customer__-acsi_card to pg
|
||||
-- requires: payment__acsi_card
|
||||
|
||||
begin;
|
||||
|
||||
alter table camper.payment_customer
|
||||
drop column if exists acsi_card
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,34 @@
|
|||
-- Deploy camper:payment_method to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table payment_method (
|
||||
payment_method_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0),
|
||||
instructions text not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table payment_method to employee;
|
||||
grant select, insert, update, delete on table payment_method to admin;
|
||||
|
||||
alter table payment_method enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on payment_method
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = payment_method.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,40 @@
|
|||
-- Deploy camper:product to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table product (
|
||||
product_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
slug uuid not null default gen_random_uuid(),
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0),
|
||||
description text not null default '',
|
||||
price integer not null,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
);
|
||||
|
||||
comment on column product.price is
|
||||
'Price is stored in cents.';
|
||||
|
||||
grant select, insert, update, delete on table product to employee;
|
||||
grant select, insert, update, delete on table product to admin;
|
||||
|
||||
alter table product enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on product
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = product.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,31 @@
|
|||
-- Deploy camper:product_tax to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: product
|
||||
-- requires: tax
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table product_tax (
|
||||
product_id integer not null references product,
|
||||
tax_id integer not null references tax,
|
||||
primary key (product_id, tax_id)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table product_tax to employee;
|
||||
grant select, insert, update, delete on table product_tax to admin;
|
||||
|
||||
alter table product_tax enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on product_tax
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from product
|
||||
where product.product_id = product_tax.product_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -6,12 +6,16 @@
|
|||
-- requires: country_code
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
-- requires: payment__acsi_card
|
||||
-- requires: payment_customer__-acsi_card
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function ready_payment(payment_slug uuid, customer_name text, customer_address text, customer_post_code text, customer_city text, customer_country_code country_code, customer_email email, customer_phone text, customer_lang_tag text, customer_acsi_card boolean) returns integer as
|
||||
drop function if exists ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean);
|
||||
|
||||
create or replace function ready_payment(payment_slug uuid, customer_name text, customer_address text, customer_post_code text, customer_city text, customer_country_code country_code, customer_email email, customer_phone text, customer_lang_tag text) returns integer as
|
||||
$$
|
||||
declare
|
||||
pid integer;
|
||||
|
@ -28,8 +32,8 @@ begin
|
|||
raise check_violation using message = 'insert or update on table "payment" violates check constraint "payment_is_draft"';
|
||||
end if;
|
||||
|
||||
insert into payment_customer (payment_id, full_name, address, postal_code, city, country_code, email, phone, acsi_card, lang_tag)
|
||||
values (pid, customer_name, customer_address, customer_post_code, customer_city, customer_country_code, customer_email, parse_packed_phone_number(customer_phone, customer_country_code), customer_acsi_card, customer_lang_tag)
|
||||
insert into payment_customer (payment_id, full_name, address, postal_code, city, country_code, email, phone, lang_tag)
|
||||
values (pid, customer_name, customer_address, customer_post_code, customer_city, customer_country_code, customer_email, parse_packed_phone_number(customer_phone, customer_country_code), customer_lang_tag)
|
||||
on conflict (payment_id) do update
|
||||
set full_name = excluded.full_name
|
||||
, address = excluded.address
|
||||
|
@ -38,7 +42,6 @@ begin
|
|||
, country_code = excluded.country_code
|
||||
, email = excluded.email
|
||||
, phone = excluded.phone
|
||||
, acsi_card = excluded.acsi_card
|
||||
, lang_tag = excluded.lang_tag
|
||||
;
|
||||
|
||||
|
@ -48,9 +51,9 @@ $$
|
|||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) from public;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to guest;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to employee;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to admin;
|
||||
revoke execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text) from public;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text) to guest;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text) to employee;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text) to admin;
|
||||
|
||||
commit;
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
-- Deploy camper:ready_payment to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: payment
|
||||
-- requires: payment_customer
|
||||
-- requires: country_code
|
||||
-- requires: email
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create or replace function ready_payment(payment_slug uuid, customer_name text, customer_address text, customer_post_code text, customer_city text, customer_country_code country_code, customer_email email, customer_phone text, customer_lang_tag text, customer_acsi_card boolean) returns integer as
|
||||
$$
|
||||
declare
|
||||
pid integer;
|
||||
begin
|
||||
update payment
|
||||
set payment_status = 'pending'
|
||||
, updated_at = current_timestamp
|
||||
where slug = payment_slug
|
||||
and payment_status = 'draft'
|
||||
returning payment_id into pid
|
||||
;
|
||||
|
||||
if pid is null then
|
||||
raise check_violation using message = 'insert or update on table "payment" violates check constraint "payment_is_draft"';
|
||||
end if;
|
||||
|
||||
insert into payment_customer (payment_id, full_name, address, postal_code, city, country_code, email, phone, acsi_card, lang_tag)
|
||||
values (pid, customer_name, customer_address, customer_post_code, customer_city, customer_country_code, customer_email, parse_packed_phone_number(customer_phone, customer_country_code), customer_acsi_card, customer_lang_tag)
|
||||
on conflict (payment_id) do update
|
||||
set full_name = excluded.full_name
|
||||
, address = excluded.address
|
||||
, postal_code = excluded.postal_code
|
||||
, city = excluded.city
|
||||
, country_code = excluded.country_code
|
||||
, email = excluded.email
|
||||
, phone = excluded.phone
|
||||
, acsi_card = excluded.acsi_card
|
||||
, lang_tag = excluded.lang_tag
|
||||
;
|
||||
|
||||
return pid;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) from public;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to guest;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to employee;
|
||||
grant execute on function ready_payment(uuid, text, text, text, text, country_code, email, text, text, boolean) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,11 @@
|
|||
-- Deploy camper:season_calendar_season_id_fkey to pg
|
||||
-- requires: season
|
||||
-- requires: season_calendar
|
||||
|
||||
begin;
|
||||
|
||||
alter table camper.season_calendar
|
||||
add constraint season_calendar_season_id_fkey foreign key (season_id) references camper.season (season_id)
|
||||
;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,17 @@
|
|||
-- Deploy camper:sex to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table sex (
|
||||
sex_id varchar(1) primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
grant select on table sex to employee;
|
||||
grant select on table sex to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,21 @@
|
|||
-- Deploy camper:sex_i18n to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: sex
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table sex_i18n (
|
||||
sex_id varchar(1) not null references sex,
|
||||
lang_tag text not null references language,
|
||||
name text not null,
|
||||
primary key (sex_id, lang_tag)
|
||||
);
|
||||
|
||||
grant select on sex_i18n to employee;
|
||||
grant select on sex_i18n to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,37 @@
|
|||
-- Deploy camper:tax to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
-- requires: tax_rate
|
||||
-- requires: tax_class
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table tax (
|
||||
tax_id integer generated by default as identity primary key,
|
||||
company_id integer not null references company,
|
||||
tax_class_id integer not null references tax_class,
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0),
|
||||
rate tax_rate not null
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table tax to employee;
|
||||
grant select, insert, update, delete on table tax to admin;
|
||||
|
||||
alter table tax enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on tax
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = tax.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,33 @@
|
|||
-- Deploy camper:tax_class to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: user_profile
|
||||
-- requires: company
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table tax_class (
|
||||
tax_class_id integer generated by default as identity not null primary key,
|
||||
company_id integer not null references company,
|
||||
name text not null constraint name_not_empty check(length(trim(name)) > 0)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on table tax_class to employee;
|
||||
grant select, insert, update, delete on table tax_class to admin;
|
||||
|
||||
alter table tax_class enable row level security;
|
||||
|
||||
create policy company_policy
|
||||
on tax_class
|
||||
using (
|
||||
exists(
|
||||
select 1
|
||||
from company_user
|
||||
join user_profile using (user_id)
|
||||
where company_user.company_id = tax_class.company_id
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
|
@ -0,0 +1,14 @@
|
|||
-- Deploy camper:tax_rate to pg
|
||||
-- requires: schema_camper
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create domain tax_rate as numeric
|
||||
check (value > -1 and value < 1);
|
||||
|
||||
comment on domain tax_rate is
|
||||
'A rate for taxes in the range (-1, 1)';
|
||||
|
||||
commit;
|
|
@ -0,0 +1,100 @@
|
|||
-- Deploy camper:unmarshal_booking to pg
|
||||
-- requires: roles
|
||||
-- requires: schema_camper
|
||||
-- requires: booking
|
||||
-- requires: booking_option
|
||||
-- requires: extension_pg_libphonenumber
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
grant select, insert on table booking to guest;
|
||||
grant select, insert on table booking_option to guest;
|
||||
|
||||
drop policy if exists delete_from_company on booking;
|
||||
drop policy if exists update_company on booking;
|
||||
drop policy if exists insert_to_company on booking;
|
||||
drop policy if exists select_from_company on booking;
|
||||
|
||||
alter table booking disable row level security;
|
||||
|
||||
create or replace function unmarshal_booking(data jsonb) returns integer as
|
||||
$$
|
||||
declare
|
||||
bid integer;
|
||||
begin
|
||||
insert into booking
|
||||
( company_id
|
||||
, campsite_type_id
|
||||
, stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, currency_code
|
||||
, zone_preferences
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, lang_tag
|
||||
)
|
||||
values((data->>'company_id')::integer
|
||||
, (data->>'campsite_type_id')::integer
|
||||
, daterange((data->>'arrival_date')::date, (data->>'departure_date')::date)
|
||||
, (data->>'subtotal_nights')::integer
|
||||
, (data->>'number_adults')::integer
|
||||
, (data->>'subtotal_adults')::integer
|
||||
, (data->>'number_teenagers')::integer
|
||||
, (data->>'subtotal_teenagers')::integer
|
||||
, (data->>'number_children')::integer
|
||||
, (data->>'subtotal_children')::integer
|
||||
, (data->>'number_dogs')::integer
|
||||
, (data->>'subtotal_dogs')::integer
|
||||
, (data->>'subtotal_tourist_tax')::integer
|
||||
, (data->>'total')::integer
|
||||
, data->>'currency_code'
|
||||
, data->>'zone_preferences'
|
||||
, (data->>'acsi_card')::boolean
|
||||
, data->>'full_name'
|
||||
, data->>'address'
|
||||
, data->>'postal_code'
|
||||
, data->>'city'
|
||||
, data->>'country_code'
|
||||
, data->>'email'
|
||||
, parse_packed_phone_number(data->>'phone', data->>'country_code')
|
||||
, data->>'lang_tag'
|
||||
)
|
||||
returning booking_id into bid;
|
||||
|
||||
if jsonb_typeof(data->'options') = 'array' then
|
||||
insert into booking_option (booking_id, campsite_type_option_id, units, subtotal)
|
||||
select bid, campsite_type_option_id, units, subtotal
|
||||
from jsonb_to_recordset(data->'options') as x(campsite_type_option_id integer, units integer, subtotal integer)
|
||||
;
|
||||
end if;
|
||||
|
||||
return bid;
|
||||
end;
|
||||
$$
|
||||
language plpgsql
|
||||
;
|
||||
|
||||
revoke execute on function unmarshal_booking(jsonb) from public;
|
||||
grant execute on function unmarshal_booking(jsonb) to guest;
|
||||
grant execute on function unmarshal_booking(jsonb) to employee;
|
||||
grant execute on function unmarshal_booking(jsonb) to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy camper:weather_forecast to pg
|
||||
-- requires: schema_camper
|
||||
-- requires: roles
|
||||
-- requires: extension_uri
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to camper, public;
|
||||
|
||||
create table weather_forecast (
|
||||
station_uri uri primary key,
|
||||
weather_condition_id text not null,
|
||||
day_temperature numeric(5,2) not null,
|
||||
min_temperature numeric(5,2) not null,
|
||||
forecasted_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
grant select on table weather_forecast to guest;
|
||||
grant select on table weather_forecast to employee;
|
||||
grant select, insert, update, delete on table weather_forecast to admin;
|
||||
|
||||
commit;
|
3
go.mod
3
go.mod
|
@ -4,15 +4,16 @@ go 1.19
|
|||
|
||||
require (
|
||||
github.com/jackc/pgconn v1.11.0
|
||||
github.com/jackc/pgio v1.0.0
|
||||
github.com/jackc/pgtype v1.10.0
|
||||
github.com/jackc/pgx/v4 v4.15.0
|
||||
github.com/leonelquinteros/gotext v1.5.0
|
||||
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8
|
||||
golang.org/x/text v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -88,6 +88,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8 h1:iZTHFqK/oFrjyFDkiw5U/RjQxkMlkpq6tHQIO407i+s=
|
||||
github.com/rainycape/unidecode v0.0.0-20150906181237-c9cf8cdbbfe8/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
|
|
|
@ -13,9 +13,11 @@ import (
|
|||
"dev.tandem.ws/tandem/camper/pkg/booking"
|
||||
"dev.tandem.ws/tandem/camper/pkg/campsite"
|
||||
"dev.tandem.ws/tandem/camper/pkg/company"
|
||||
"dev.tandem.ws/tandem/camper/pkg/customer"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/home"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/invoice"
|
||||
"dev.tandem.ws/tandem/camper/pkg/legal"
|
||||
"dev.tandem.ws/tandem/camper/pkg/location"
|
||||
"dev.tandem.ws/tandem/camper/pkg/media"
|
||||
|
@ -32,11 +34,14 @@ type adminHandler struct {
|
|||
booking *booking.AdminHandler
|
||||
campsite *campsite.AdminHandler
|
||||
company *company.AdminHandler
|
||||
customer *customer.AdminHandler
|
||||
home *home.AdminHandler
|
||||
invoice *invoice.AdminHandler
|
||||
legal *legal.AdminHandler
|
||||
location *location.AdminHandler
|
||||
media *media.AdminHandler
|
||||
payment *payment.AdminHandler
|
||||
prebooking *booking.PrebookingHandler
|
||||
season *season.AdminHandler
|
||||
services *services.AdminHandler
|
||||
surroundings *surroundings.AdminHandler
|
||||
|
@ -49,11 +54,14 @@ func newAdminHandler(mediaDir string) *adminHandler {
|
|||
booking: booking.NewAdminHandler(),
|
||||
campsite: campsite.NewAdminHandler(),
|
||||
company: company.NewAdminHandler(),
|
||||
customer: customer.NewAdminHandler(),
|
||||
home: home.NewAdminHandler(),
|
||||
invoice: invoice.NewAdminHandler(),
|
||||
legal: legal.NewAdminHandler(),
|
||||
location: location.NewAdminHandler(),
|
||||
media: media.NewAdminHandler(mediaDir),
|
||||
payment: payment.NewAdminHandler(),
|
||||
prebooking: booking.NewPrebookingHandler(),
|
||||
season: season.NewAdminHandler(),
|
||||
services: services.NewAdminHandler(),
|
||||
surroundings: surroundings.NewAdminHandler(),
|
||||
|
@ -85,16 +93,22 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
|
|||
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "company":
|
||||
h.company.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "customers":
|
||||
h.customer.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "home":
|
||||
h.home.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "legal":
|
||||
h.legal.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "invoices":
|
||||
h.invoice.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "location":
|
||||
h.location.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "media":
|
||||
h.media.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "payments":
|
||||
h.payment.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "prebookings":
|
||||
h.prebooking.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "seasons":
|
||||
h.season.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "services":
|
||||
|
|
|
@ -7,15 +7,23 @@ package booking
|
|||
|
||||
import (
|
||||
"context"
|
||||
"golang.org/x/text/language"
|
||||
"dev.tandem.ws/tandem/camper/pkg/ods"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
|
@ -35,6 +43,91 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
|||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveBookingIndex(w, r, user, company, conn)
|
||||
case http.MethodPost:
|
||||
addBooking(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||
}
|
||||
case "new":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveAdminBookingForm(w, r, user, company, conn, 0, "/admin/bookings/new")
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
if !uuid.Valid(head) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
h.bookingHandler(user, company, conn, head).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func serveAdminBookingForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, id int, url string) {
|
||||
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.ID = id
|
||||
f.URL = url
|
||||
f.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) bookingHandler(user *auth.User, company *auth.Company, conn *database.Conn, slug string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(r.Form) > 0 {
|
||||
// Act as if it was a new form, because everything needed to render form fields is
|
||||
// already passed as in request query.
|
||||
id, err := conn.GetInt(r.Context(), "select booking_id from booking where slug = $1", slug)
|
||||
if err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
serveAdminBookingForm(w, r, user, company, conn, id, "/admin/bookings/"+slug)
|
||||
return
|
||||
}
|
||||
f := newEmptyAdminBookingForm(r.Context(), conn, company, user.Locale)
|
||||
if err := f.FillFromDatabase(r.Context(), conn, company, slug, user.Locale); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
case http.MethodPut:
|
||||
updateBooking(w, r, user, company, conn, slug)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
case "check-in":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveCheckInForm(w, r, user, company, conn, slug)
|
||||
case http.MethodPost:
|
||||
checkInBooking(w, r, user, company, conn, slug)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||
}
|
||||
case "guest":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveGuestForm(w, r, user, company, conn, slug)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
|
@ -45,28 +138,41 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
|||
}
|
||||
|
||||
func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language)
|
||||
filters := newFilterForm(r.Context(), conn, company, user.Locale)
|
||||
if err := filters.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language, filters)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
page := bookingIndex(bookings)
|
||||
page := &bookingIndex{
|
||||
Bookings: filters.buildCursor(bookings),
|
||||
Filters: filters,
|
||||
}
|
||||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select left(slug::text, 10)
|
||||
, '/admin/booking/' || slug
|
||||
, arrival_date
|
||||
, departure_date
|
||||
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag, filters *filterForm) ([]*bookingEntry, error) {
|
||||
where, args := filters.BuildQuery([]interface{}{lang.String()})
|
||||
rows, err := conn.Query(ctx, fmt.Sprintf(`
|
||||
select booking_id
|
||||
, left(slug::text, 10)
|
||||
, '/admin/bookings/' || slug
|
||||
, lower(stay)
|
||||
, upper(stay)
|
||||
, holder_name
|
||||
, booking.booking_status
|
||||
, coalesce(i18n.name, status.name)
|
||||
from booking
|
||||
join booking_status as status using (booking_status)
|
||||
left join booking_status_i18n as i18n on status.booking_status = i18n.booking_status and i18n.lang_tag = $1
|
||||
order by arrival_date desc
|
||||
`, lang)
|
||||
where (%s)
|
||||
order by lower(stay) desc
|
||||
, booking_id desc
|
||||
LIMIT %d
|
||||
`, where, filters.PerPage()+1), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -75,7 +181,7 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua
|
|||
var entries []*bookingEntry
|
||||
for rows.Next() {
|
||||
entry := &bookingEntry{}
|
||||
if err = rows.Scan(&entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil {
|
||||
if err = rows.Scan(&entry.ID, &entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
|
@ -85,6 +191,7 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua
|
|||
}
|
||||
|
||||
type bookingEntry struct {
|
||||
ID int
|
||||
Reference string
|
||||
URL string
|
||||
ArrivalDate time.Time
|
||||
|
@ -94,7 +201,10 @@ type bookingEntry struct {
|
|||
StatusLabel string
|
||||
}
|
||||
|
||||
type bookingIndex []*bookingEntry
|
||||
type bookingIndex struct {
|
||||
Bookings []*bookingEntry
|
||||
Filters *filterForm
|
||||
}
|
||||
|
||||
func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
switch r.URL.Query().Get("format") {
|
||||
|
@ -106,16 +216,16 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
|||
"Holder Name",
|
||||
"Status",
|
||||
}
|
||||
ods, err := writeTableOds(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
|
||||
if err := writeCellString(sb, entry.Reference); err != nil {
|
||||
table, err := ods.WriteTable(page.Bookings, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
|
||||
if err := ods.WriteCellString(sb, entry.Reference); err != nil {
|
||||
return err
|
||||
}
|
||||
writeCellDate(sb, entry.ArrivalDate)
|
||||
writeCellDate(sb, entry.DepartureDate)
|
||||
if err := writeCellString(sb, entry.HolderName); err != nil {
|
||||
ods.WriteCellDate(sb, entry.ArrivalDate)
|
||||
ods.WriteCellDate(sb, entry.DepartureDate)
|
||||
if err := ods.WriteCellString(sb, entry.HolderName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeCellString(sb, entry.StatusLabel); err != nil {
|
||||
if err := ods.WriteCellString(sb, entry.StatusLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -123,8 +233,432 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mustWriteOdsResponse(w, ods, user.Locale.Pgettext("bookings.ods", "filename"))
|
||||
ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
|
||||
default:
|
||||
template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
|
||||
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "booking/results.gohtml", page)
|
||||
} else {
|
||||
template.MustRenderAdminFiles(w, r, user, company, page, "booking/index.gohtml", "booking/results.gohtml")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type adminBookingForm struct {
|
||||
*bookingForm
|
||||
ID int
|
||||
URL string
|
||||
Status string
|
||||
Campsites []*CampsiteEntry
|
||||
selected []int
|
||||
Months []*Month
|
||||
Error error
|
||||
}
|
||||
|
||||
func newEmptyAdminBookingForm(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) *adminBookingForm {
|
||||
return &adminBookingForm{
|
||||
bookingForm: newEmptyBookingForm(ctx, conn, company, l),
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminBookingForm(r *http.Request, conn *database.Conn, company *auth.Company, l *locale.Locale) (*adminBookingForm, error) {
|
||||
inner, err := newBookingForm(r, company, conn, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inner.Options != nil {
|
||||
for _, option := range inner.Options.Options {
|
||||
option.Subtotal = findSubtotal(option.ID, inner.Cart)
|
||||
}
|
||||
}
|
||||
f := &adminBookingForm{
|
||||
bookingForm: inner,
|
||||
}
|
||||
// Dates and Campsite are valid
|
||||
if inner.Guests != nil {
|
||||
selected := r.Form["campsite"]
|
||||
if err = f.FetchCampsites(r.Context(), conn, company, selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) FetchCampsites(ctx context.Context, conn *database.Conn, company *auth.Company, selected []string) error {
|
||||
arrivalDate, _ := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
||||
from := arrivalDate.AddDate(0, 0, -1)
|
||||
departureDate, _ := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
||||
to := departureDate.AddDate(0, 0, 2)
|
||||
f.Months = CollectMonths(from, to)
|
||||
var err error
|
||||
f.Campsites, err = CollectCampsiteEntries(ctx, company, conn, from, to, f.CampsiteType.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range selected {
|
||||
ID, _ := strconv.Atoi(s)
|
||||
for _, c := range f.Campsites {
|
||||
if c.ID == ID {
|
||||
f.selected = append(f.selected, c.ID)
|
||||
c.Selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSubtotal(ID int, cart *bookingCart) string {
|
||||
none := "0.0"
|
||||
if cart == nil || cart.Draft == nil {
|
||||
return none
|
||||
}
|
||||
for _, option := range cart.Draft.Options {
|
||||
if option.ID == ID {
|
||||
return option.Subtotal
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
if httplib.IsHTMxRequest(r) {
|
||||
template.MustRenderAdminNoLayoutFiles(w, r, user, company, f, "booking/fields.gohtml", "booking/grid.gohtml")
|
||||
} else {
|
||||
template.MustRenderAdminFiles(w, r, user, company, f, "booking/form.gohtml", "booking/fields.gohtml", "booking/grid.gohtml")
|
||||
}
|
||||
}
|
||||
|
||||
func addBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
processAdminBookingForm(w, r, user, company, conn, 0, func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
||||
var err error
|
||||
f.ID, err = tx.AddBookingFromPayment(ctx, f.PaymentSlug.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.EditBooking(
|
||||
ctx,
|
||||
f.ID,
|
||||
f.Customer.FullName.Val,
|
||||
f.Customer.Address.Val,
|
||||
f.Customer.PostalCode.Val,
|
||||
f.Customer.City.Val,
|
||||
f.Customer.Country.String(),
|
||||
f.Customer.Email.Val,
|
||||
f.Customer.Phone.Val,
|
||||
language.Make("und"),
|
||||
"confirmed",
|
||||
f.selected,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func updateBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
|
||||
var bookingID int
|
||||
var bookingStatus string
|
||||
var langTag string
|
||||
row := conn.QueryRow(r.Context(), "select booking_id, booking_status, lang_tag from booking where slug = $1", slug)
|
||||
if err := row.Scan(&bookingID, &bookingStatus, &langTag); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(r.Form["cancel"]) > 0 {
|
||||
if err := conn.CancelBooking(r.Context(), bookingID); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if bookingStatus == "created" {
|
||||
httplib.Redirect(w, r, "/admin/prebookings", http.StatusSeeOther)
|
||||
} else {
|
||||
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
|
||||
}
|
||||
return
|
||||
}
|
||||
processAdminBookingForm(w, r, user, company, conn, bookingID, func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error {
|
||||
var err error
|
||||
_, err = tx.EditBookingFromPayment(ctx, slug, f.PaymentSlug.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bookingStatus == "created" {
|
||||
bookingStatus = "confirmed"
|
||||
}
|
||||
return tx.EditBooking(
|
||||
ctx,
|
||||
f.ID,
|
||||
f.Customer.FullName.Val,
|
||||
f.Customer.Address.Val,
|
||||
f.Customer.PostalCode.Val,
|
||||
f.Customer.City.Val,
|
||||
f.Customer.Country.String(),
|
||||
f.Customer.Email.Val,
|
||||
f.Customer.Phone.Val,
|
||||
language.Make(langTag),
|
||||
bookingStatus,
|
||||
f.selected,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func processAdminBookingForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, bookingID int, act func(ctx context.Context, tx *database.Tx, f *adminBookingForm) error) {
|
||||
f, err := newAdminBookingForm(r, conn, company, user.Locale)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
f.ID = bookingID
|
||||
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
|
||||
panic(err)
|
||||
} else if !ok {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
|
||||
tx := conn.MustBegin(r.Context())
|
||||
defer tx.Rollback(r.Context())
|
||||
if err := act(r.Context(), tx, f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tx.MustCommit(r.Context())
|
||||
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
||||
v := form.NewValidator(l)
|
||||
|
||||
if f.Dates == nil {
|
||||
return false, errors.New("no booking date fields")
|
||||
}
|
||||
if f.Guests == nil {
|
||||
return false, errors.New("no guests fields")
|
||||
}
|
||||
if f.Customer == nil {
|
||||
return false, errors.New("no customer fields")
|
||||
}
|
||||
if f.Cart == nil {
|
||||
return false, errors.New("no booking cart")
|
||||
}
|
||||
|
||||
v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
|
||||
f.Dates.Valid(v, l)
|
||||
f.Guests.Valid(v, l)
|
||||
if f.Options != nil {
|
||||
f.Options.Valid(v, l)
|
||||
}
|
||||
|
||||
var country string
|
||||
if f.Customer.Country.ValidOptionsSelected() {
|
||||
country = f.Customer.Country.Selected[0]
|
||||
}
|
||||
|
||||
if v.CheckRequired(f.Customer.FullName, l.GettextNoop("Full name can not be empty.")) {
|
||||
v.CheckMinLength(f.Customer.FullName, 1, l.GettextNoop("Full name must have at least one letter."))
|
||||
}
|
||||
|
||||
if f.Customer.PostalCode.Val != "" {
|
||||
if country == "" {
|
||||
v.Check(f.Customer.PostalCode, false, l.GettextNoop("Country can not be empty to validate the postcode."))
|
||||
} else if _, err := v.CheckValidPostalCode(ctx, conn, f.Customer.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if f.Customer.Email.Val != "" {
|
||||
v.CheckValidEmail(f.Customer.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
|
||||
}
|
||||
if f.Customer.Phone.Val != "" {
|
||||
if country == "" {
|
||||
v.Check(f.Customer.Phone, false, l.GettextNoop("Country can not be empty to validate the phone."))
|
||||
} else if _, err := v.CheckValidPhone(ctx, conn, f.Customer.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.selected) == 0 {
|
||||
f.Error = errors.New(l.Gettext("You must select at least one accommodation."))
|
||||
v.AllOK = false
|
||||
} else if f.Dates.ArrivalDate.Error == nil && f.Dates.DepartureDate.Error == nil {
|
||||
if available, err := datesAvailable(ctx, conn, f.ID, f.Dates, f.selected); err != nil {
|
||||
return false, err
|
||||
} else if !available {
|
||||
f.Error = errors.New(l.Gettext("The selected accommodations have no available openings in the requested dates."))
|
||||
v.AllOK = false
|
||||
}
|
||||
}
|
||||
|
||||
return v.AllOK, nil
|
||||
}
|
||||
|
||||
func datesAvailable(ctx context.Context, conn *database.Conn, bookingID int, dates *DateFields, selectedCampsites []int) (bool, error) {
|
||||
return conn.GetBool(ctx, `
|
||||
select not exists (
|
||||
select 1
|
||||
from camper.booking_campsite
|
||||
where booking_id <> $1
|
||||
and campsite_id = any ($4)
|
||||
and stay && daterange($2::date, $3::date)
|
||||
)
|
||||
`,
|
||||
bookingID,
|
||||
dates.ArrivalDate,
|
||||
dates.DepartureDate,
|
||||
selectedCampsites,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *adminBookingForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, slug string, l *locale.Locale) error {
|
||||
f.Cart = &bookingCart{Draft: &paymentDraft{}}
|
||||
f.Customer = newBookingCustomerFields(ctx, conn, l)
|
||||
var arrivalDate string
|
||||
var departureDate string
|
||||
var acsiCard bool
|
||||
var zonePreferences string
|
||||
var selected []string
|
||||
row := conn.QueryRow(ctx, `
|
||||
select booking_id
|
||||
, '/admin/bookings/' || booking.slug
|
||||
, booking_status
|
||||
, array[campsite_type.slug::text]
|
||||
, lower(booking.stay)::text
|
||||
, upper(booking.stay)::text
|
||||
, upper(booking.stay) - lower(booking.stay)
|
||||
, to_price(subtotal_nights, decimal_digits)
|
||||
, number_adults
|
||||
, to_price(subtotal_adults, decimal_digits)
|
||||
, number_teenagers
|
||||
, to_price(subtotal_teenagers, decimal_digits)
|
||||
, number_children
|
||||
, to_price(subtotal_children, decimal_digits)
|
||||
, number_dogs
|
||||
, to_price(subtotal_dogs, decimal_digits)
|
||||
, to_price(subtotal_tourist_tax, decimal_digits)
|
||||
, to_price(total, decimal_digits)
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, coalesce(address, '')
|
||||
, coalesce(postal_code, '')
|
||||
, coalesce(city, '')
|
||||
, array[coalesce(country_code::text, '')]
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
, zone_preferences
|
||||
, array_agg(coalesce(campsite_id::text, ''))
|
||||
from booking
|
||||
join campsite_type using (campsite_type_id)
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
left join booking_campsite using (booking_id)
|
||||
join currency using (currency_code)
|
||||
where booking.slug = $1
|
||||
group by booking_id
|
||||
, campsite_type.slug
|
||||
, booking.stay
|
||||
, subtotal_nights
|
||||
, number_adults
|
||||
, subtotal_adults
|
||||
, number_teenagers
|
||||
, subtotal_teenagers
|
||||
, number_children
|
||||
, subtotal_children
|
||||
, number_dogs
|
||||
, subtotal_dogs
|
||||
, subtotal_tourist_tax
|
||||
, total
|
||||
, acsi_card
|
||||
, holder_name
|
||||
, address
|
||||
, postal_code
|
||||
, city
|
||||
, country_code
|
||||
, email
|
||||
, phone
|
||||
, zone_preferences
|
||||
, decimal_digits
|
||||
`, slug)
|
||||
if err := row.Scan(
|
||||
&f.ID,
|
||||
&f.URL,
|
||||
&f.Status,
|
||||
&f.CampsiteType.Selected,
|
||||
&arrivalDate,
|
||||
&departureDate,
|
||||
&f.Cart.Draft.NumNights,
|
||||
&f.Cart.Draft.Nights,
|
||||
&f.Cart.Draft.NumAdults,
|
||||
&f.Cart.Draft.Adults,
|
||||
&f.Cart.Draft.NumTeenagers,
|
||||
&f.Cart.Draft.Teenagers,
|
||||
&f.Cart.Draft.NumChildren,
|
||||
&f.Cart.Draft.Children,
|
||||
&f.Cart.Draft.NumDogs,
|
||||
&f.Cart.Draft.Dogs,
|
||||
&f.Cart.Draft.TouristTax,
|
||||
&f.Cart.Draft.Total,
|
||||
&acsiCard,
|
||||
&f.Customer.FullName.Val,
|
||||
&f.Customer.Address.Val,
|
||||
&f.Customer.PostalCode.Val,
|
||||
&f.Customer.City.Val,
|
||||
&f.Customer.Country.Selected,
|
||||
&f.Customer.Email.Val,
|
||||
&f.Customer.Phone.Val,
|
||||
&zonePreferences,
|
||||
&selected,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
f.Dates, err = NewDateFields(ctx, conn, f.CampsiteType.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Dates.ArrivalDate.Val = arrivalDate
|
||||
f.Dates.DepartureDate.Val = departureDate
|
||||
f.Dates.AdjustValues(l)
|
||||
|
||||
f.Guests, err = newBookingGuestFields(ctx, conn, f.CampsiteType.String(), arrivalDate, departureDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Guests.NumberAdults.Val = strconv.Itoa(f.Cart.Draft.NumAdults)
|
||||
f.Guests.NumberTeenagers.Val = strconv.Itoa(f.Cart.Draft.NumTeenagers)
|
||||
f.Guests.NumberChildren.Val = strconv.Itoa(f.Cart.Draft.NumChildren)
|
||||
if f.Guests.NumberDogs != nil {
|
||||
f.Guests.NumberDogs.Val = strconv.Itoa(f.Cart.Draft.NumDogs)
|
||||
}
|
||||
if f.Guests.ACSICard != nil {
|
||||
f.Guests.ACSICard.Checked = acsiCard
|
||||
}
|
||||
f.Guests.AdjustValues(f.Cart.Draft.NumAdults+f.Cart.Draft.NumTeenagers+f.Cart.Draft.NumChildren, l)
|
||||
|
||||
f.Options, err = newBookingOptionFields(ctx, conn, f.CampsiteType.String(), l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Options != nil {
|
||||
if f.Options.ZonePreferences != nil {
|
||||
f.Options.ZonePreferences.Val = zonePreferences
|
||||
}
|
||||
if err = f.Options.FillFromDatabase(ctx, conn, f.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = f.FetchCampsites(ctx, conn, company, selected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"time"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/season"
|
||||
)
|
||||
|
||||
type Month struct {
|
||||
Year int
|
||||
Month time.Month
|
||||
Name string
|
||||
Days []time.Time
|
||||
Spans []*Span
|
||||
}
|
||||
|
||||
type Span struct {
|
||||
Weekend bool
|
||||
Today bool
|
||||
Count int
|
||||
}
|
||||
|
||||
func isWeekend(t time.Time) bool {
|
||||
switch t.Weekday() {
|
||||
case time.Saturday, time.Sunday:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func CollectMonths(from time.Time, to time.Time) []*Month {
|
||||
current := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC)
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
var months []*Month
|
||||
for !current.Equal(to) {
|
||||
span := &Span{
|
||||
Weekend: isWeekend(current),
|
||||
Today: current.Equal(today),
|
||||
}
|
||||
month := &Month{
|
||||
Year: current.Year(),
|
||||
Month: current.Month(),
|
||||
Name: season.LongMonthNames[current.Month()-1],
|
||||
Days: make([]time.Time, 0, 31),
|
||||
Spans: make([]*Span, 0, 10),
|
||||
}
|
||||
month.Spans = append(month.Spans, span)
|
||||
for current.Month() == month.Month && !current.Equal(to) {
|
||||
month.Days = append(month.Days, current)
|
||||
if span.Weekend != isWeekend(current) || span.Today != current.Equal(today) {
|
||||
span = &Span{
|
||||
Weekend: isWeekend(current),
|
||||
Today: current.Equal(today),
|
||||
}
|
||||
month.Spans = append(month.Spans, span)
|
||||
}
|
||||
span.Count = span.Count + 1
|
||||
current = current.AddDate(0, 0, 1)
|
||||
}
|
||||
months = append(months, month)
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
type CampsiteEntry struct {
|
||||
ID int
|
||||
Label string
|
||||
Type string
|
||||
TypeSlug string
|
||||
Active bool
|
||||
Selected bool
|
||||
Bookings map[time.Time]*CampsiteBooking
|
||||
}
|
||||
|
||||
type CampsiteBooking struct {
|
||||
URL string
|
||||
Holder string
|
||||
Status string
|
||||
Nights int
|
||||
Begin bool
|
||||
End bool
|
||||
}
|
||||
|
||||
func CollectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsiteType string) ([]*CampsiteEntry, error) {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite_id
|
||||
, campsite.label
|
||||
, campsite_type.name
|
||||
, campsite_type.slug
|
||||
, campsite.active
|
||||
from campsite
|
||||
join campsite_type using (campsite_type_id)
|
||||
where campsite.company_id = $1
|
||||
and ($2::uuid is null or campsite_type.slug = $2::uuid)
|
||||
order by label`, company.ID, database.ZeroNullUUID(campsiteType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
byLabel := make(map[string]*CampsiteEntry)
|
||||
var campsites []*CampsiteEntry
|
||||
for rows.Next() {
|
||||
entry := &CampsiteEntry{}
|
||||
if err = rows.Scan(&entry.ID, &entry.Label, &entry.Type, &entry.TypeSlug, &entry.Active); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
campsites = append(campsites, entry)
|
||||
byLabel[entry.Label] = entry
|
||||
}
|
||||
|
||||
if err := collectCampsiteBookings(ctx, company, conn, from, to, byLabel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return campsites, nil
|
||||
}
|
||||
|
||||
func collectCampsiteBookings(ctx context.Context, company *auth.Company, conn *database.Conn, from time.Time, to time.Time, campsites map[string]*CampsiteEntry) error {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite.label
|
||||
, lower(booking_campsite.stay * daterange($2::date, $3::date))
|
||||
, '/admin/bookings/' || booking.slug
|
||||
, holder_name
|
||||
, booking_status
|
||||
, upper(booking_campsite.stay * daterange($2::date, $3::date)) - lower(booking_campsite.stay * daterange($2::date, $3::date))
|
||||
, booking_campsite.stay &> daterange($2::date, $3::date)
|
||||
, booking_campsite.stay &< daterange($2::date, ($3 - 1)::date)
|
||||
from booking_campsite
|
||||
join booking using (booking_id)
|
||||
join campsite using (campsite_id)
|
||||
where booking.company_id = $1
|
||||
and booking_campsite.stay && daterange($2::date, $3::date)
|
||||
order by label`, company.ID, from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
entry := &CampsiteBooking{}
|
||||
var label string
|
||||
var date time.Time
|
||||
if err = rows.Scan(&label, &date, &entry.URL, &entry.Holder, &entry.Status, &entry.Nights, &entry.Begin, &entry.End); err != nil {
|
||||
return err
|
||||
}
|
||||
campsite := campsites[label]
|
||||
if campsite != nil {
|
||||
if campsite.Bookings == nil {
|
||||
campsite.Bookings = make(map[time.Time]*CampsiteBooking)
|
||||
}
|
||||
campsite.Bookings[date] = entry
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
type bookingCart struct {
|
||||
Draft *paymentDraft
|
||||
Lines []*cartLine
|
||||
Total string
|
||||
DownPayment string
|
||||
|
@ -23,49 +24,79 @@ type cartLine struct {
|
|||
Subtotal string
|
||||
}
|
||||
|
||||
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
|
||||
cart := &bookingCart{
|
||||
Total: "0.0",
|
||||
}
|
||||
type paymentDraft struct {
|
||||
NumAdults int
|
||||
NumTeenagers int
|
||||
NumChildren int
|
||||
NumDogs int
|
||||
NumNights int
|
||||
Nights string
|
||||
Adults string
|
||||
Teenagers string
|
||||
Children string
|
||||
Dogs string
|
||||
TouristTax string
|
||||
Total string
|
||||
DownPaymentPercent int
|
||||
DownPayment string
|
||||
Options []*paymentOption
|
||||
}
|
||||
|
||||
type paymentOption struct {
|
||||
ID int
|
||||
Label string
|
||||
Units int
|
||||
Subtotal string
|
||||
}
|
||||
|
||||
func draftPayment(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*paymentDraft, error) {
|
||||
if f.Dates == nil {
|
||||
return cart, nil
|
||||
}
|
||||
arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
}
|
||||
departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if f.Guests == nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
numAdults, err := strconv.Atoi(f.Guests.NumberAdults.Val)
|
||||
|
||||
arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
numTeenagers, err := strconv.Atoi(f.Guests.NumberTeenagers.Val)
|
||||
departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
numChildren, err := strconv.Atoi(f.Guests.NumberChildren.Val)
|
||||
|
||||
draft := &paymentDraft{}
|
||||
draft.NumAdults, err = strconv.Atoi(f.Guests.NumberAdults.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
draft.NumTeenagers, err = strconv.Atoi(f.Guests.NumberTeenagers.Val)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
draft.NumChildren, err = strconv.Atoi(f.Guests.NumberChildren.Val)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
numDogs := 0
|
||||
if f.Guests.NumberDogs != nil {
|
||||
numDogs, err = strconv.Atoi(f.Guests.NumberDogs.Val)
|
||||
draft.NumDogs, err = strconv.Atoi(f.Guests.NumberDogs.Val)
|
||||
if err != nil {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
zonePreferences := ""
|
||||
|
||||
var zonePreferences string
|
||||
if f.Options != nil && f.Options.ZonePreferences != nil {
|
||||
zonePreferences = f.Options.ZonePreferences.Val
|
||||
}
|
||||
|
||||
var acsiCard bool
|
||||
if f.Guests.ACSICard != nil {
|
||||
acsiCard = f.Guests.ACSICard.Checked
|
||||
}
|
||||
|
||||
optionMap := make(map[int]*campsiteTypeOption)
|
||||
var typeOptions []*campsiteTypeOption
|
||||
if f.Options != nil {
|
||||
|
@ -97,65 +128,42 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
|||
, to_price(total, decimal_digits)
|
||||
, to_price(payment.down_payment, decimal_digits)
|
||||
, (payment.down_payment_percent * 100)::int
|
||||
from draft_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) as payment
|
||||
from draft_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) as payment
|
||||
join currency using (currency_code)
|
||||
`,
|
||||
database.ZeroNullUUID(f.PaymentSlug.Val),
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
campsiteType,
|
||||
numAdults,
|
||||
numTeenagers,
|
||||
numChildren,
|
||||
numDogs,
|
||||
draft.NumAdults,
|
||||
draft.NumTeenagers,
|
||||
draft.NumChildren,
|
||||
draft.NumDogs,
|
||||
zonePreferences,
|
||||
acsiCard,
|
||||
database.OptionUnitsArray(optionUnits),
|
||||
)
|
||||
var paymentID int
|
||||
var numNights int
|
||||
var nights string
|
||||
var adults string
|
||||
var teenagers string
|
||||
var children string
|
||||
var dogs string
|
||||
var touristTax string
|
||||
var total string
|
||||
var downPayment string
|
||||
if err = row.Scan(
|
||||
&f.PaymentSlug.Val,
|
||||
&paymentID,
|
||||
&numNights,
|
||||
&nights,
|
||||
&adults,
|
||||
&teenagers,
|
||||
&children,
|
||||
&dogs,
|
||||
&touristTax,
|
||||
&total,
|
||||
&downPayment,
|
||||
&cart.DownPaymentPercent,
|
||||
&draft.NumNights,
|
||||
&draft.Nights,
|
||||
&draft.Adults,
|
||||
&draft.Teenagers,
|
||||
&draft.Children,
|
||||
&draft.Dogs,
|
||||
&draft.TouristTax,
|
||||
&draft.Total,
|
||||
&draft.DownPayment,
|
||||
&draft.DownPaymentPercent,
|
||||
); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
return cart, nil
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maybeAddLine := func(units int, subtotal string, concept string) {
|
||||
if units > 0 && subtotal != "" {
|
||||
cart.Lines = append(cart.Lines, &cartLine{
|
||||
Concept: concept,
|
||||
Units: units,
|
||||
Subtotal: subtotal,
|
||||
})
|
||||
}
|
||||
}
|
||||
maybeAddLine(numNights, nights, locale.PgettextNoop("Night", "cart"))
|
||||
maybeAddLine(numAdults, adults, locale.PgettextNoop("Adult", "cart"))
|
||||
maybeAddLine(numTeenagers, teenagers, locale.PgettextNoop("Teenager", "cart"))
|
||||
maybeAddLine(numChildren, children, locale.PgettextNoop("Child", "cart"))
|
||||
maybeAddLine(numDogs, dogs, locale.PgettextNoop("Dog", "cart"))
|
||||
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite_type_option_id
|
||||
, units
|
||||
|
@ -164,6 +172,7 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
|||
join payment using (payment_id)
|
||||
join currency using (currency_code)
|
||||
where payment_id = $1
|
||||
order by campsite_type_option_id
|
||||
`, paymentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -182,20 +191,62 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
|
|||
if option == nil {
|
||||
continue
|
||||
}
|
||||
maybeAddLine(units, subtotal, option.Label)
|
||||
draft.Options = append(draft.Options, &paymentOption{
|
||||
ID: option.ID,
|
||||
Label: option.Label,
|
||||
Units: units,
|
||||
Subtotal: subtotal,
|
||||
})
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, rows.Err()
|
||||
}
|
||||
|
||||
maybeAddLine(numAdults, touristTax, locale.PgettextNoop("Tourist tax", "cart"))
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
if total != "0.0" {
|
||||
cart.Total = total
|
||||
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
|
||||
cart := &bookingCart{
|
||||
Total: "0.0",
|
||||
}
|
||||
|
||||
draft, err := draftPayment(ctx, conn, f, campsiteType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if draft == nil {
|
||||
return cart, nil
|
||||
}
|
||||
cart.Draft = draft
|
||||
cart.DownPaymentPercent = draft.DownPaymentPercent
|
||||
|
||||
maybeAddLine := func(units int, subtotal string, concept string) {
|
||||
if units > 0 && subtotal != "" {
|
||||
cart.Lines = append(cart.Lines, &cartLine{
|
||||
Concept: concept,
|
||||
Units: units,
|
||||
Subtotal: subtotal,
|
||||
})
|
||||
}
|
||||
}
|
||||
maybeAddLine(draft.NumNights, draft.Nights, locale.PgettextNoop("Night", "cart"))
|
||||
maybeAddLine(draft.NumAdults, draft.Adults, locale.PgettextNoop("Adult", "cart"))
|
||||
maybeAddLine(draft.NumTeenagers, draft.Teenagers, locale.PgettextNoop("Teenager", "cart"))
|
||||
maybeAddLine(draft.NumChildren, draft.Children, locale.PgettextNoop("Child", "cart"))
|
||||
maybeAddLine(draft.NumDogs, draft.Dogs, locale.PgettextNoop("Dog", "cart"))
|
||||
|
||||
for _, option := range draft.Options {
|
||||
maybeAddLine(option.Units, option.Subtotal, option.Label)
|
||||
}
|
||||
|
||||
maybeAddLine(draft.NumAdults, draft.TouristTax, locale.PgettextNoop("Tourist tax", "cart"))
|
||||
|
||||
if draft.Total != "0.0" {
|
||||
cart.Total = draft.Total
|
||||
cart.Enabled = f.Guests.Error == nil
|
||||
|
||||
if downPayment != total {
|
||||
cart.DownPayment = downPayment
|
||||
if draft.DownPayment != draft.Total {
|
||||
cart.DownPayment = draft.DownPayment
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func serveCheckInForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
|
||||
f := newCheckinForm(slug)
|
||||
if err := f.FillFromDatabase(r.Context(), conn, user.Locale); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func serveGuestForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
|
||||
f, err := newGuestForm(r.Context(), conn, user.Locale, slug)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func checkInBooking(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slug string) {
|
||||
f := newCheckinForm(slug)
|
||||
if err := f.Parse(r, user, conn); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
|
||||
panic(err)
|
||||
} else if !ok {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
guests := make([]*database.CheckedInGuest, 0, len(f.Guests))
|
||||
for _, g := range f.Guests {
|
||||
guests = append(guests, g.checkedInGuest())
|
||||
}
|
||||
if err := conn.CheckInGuests(r.Context(), f.Slug, guests); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
httplib.Redirect(w, r, "/admin/bookings", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type checkInForm struct {
|
||||
Slug string
|
||||
Guests []*guestForm
|
||||
}
|
||||
|
||||
func newCheckinForm(slug string) *checkInForm {
|
||||
return &checkInForm{
|
||||
Slug: slug,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *checkInForm) FillFromDatabase(ctx context.Context, conn *database.Conn, l *locale.Locale) error {
|
||||
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
|
||||
sexes := mustGetSexOptions(ctx, conn, l)
|
||||
countries := form.MustGetCountryOptions(ctx, conn, l)
|
||||
|
||||
rows, err := conn.Query(ctx, `
|
||||
select array[id_document_type_id]
|
||||
, id_document_number
|
||||
, coalesce(id_document_issue_date::text, '')
|
||||
, given_name
|
||||
, first_surname
|
||||
, second_surname
|
||||
, array[sex_id]
|
||||
, birthdate::text
|
||||
, array[guest.country_code::text]
|
||||
, coalesce(guest.phone::text, '')
|
||||
, guest.address
|
||||
from booking_guest as guest
|
||||
join booking using (booking_id)
|
||||
where slug = $1
|
||||
`, f.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
guest := newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
|
||||
if err := guest.FillFromRow(rows); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Guests = append(f.Guests, guest)
|
||||
}
|
||||
if len(f.Guests) == 0 {
|
||||
var numberGuests int
|
||||
var address string
|
||||
var country []string
|
||||
row := conn.QueryRow(ctx, "select number_adults + number_teenagers, coalesce(address, ''), array[coalesce(country_code, '')] from booking where slug = $1", f.Slug)
|
||||
if err = row.Scan(&numberGuests, &address, &country); err != nil {
|
||||
return err
|
||||
}
|
||||
guests := make([]*guestForm, 0, numberGuests)
|
||||
for i := 0; i < numberGuests; i++ {
|
||||
guests = append(guests, newGuestFormWithOptions(documentTypes, sexes, countries, address, country))
|
||||
}
|
||||
f.Guests = guests
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustGetSexOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*form.Option {
|
||||
return form.MustGetOptions(ctx, conn, "select sex.sex_id::text, coalesce(i18n.name, sex.name) as l10n_name from sex left join sex_i18n as i18n on sex.sex_id = i18n.sex_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
|
||||
}
|
||||
|
||||
func (f *checkInForm) Parse(r *http.Request, user *auth.User, conn *database.Conn) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
documentTypes := form.MustGetDocumentTypeOptions(r.Context(), conn, user.Locale)
|
||||
sexes := mustGetSexOptions(r.Context(), conn, user.Locale)
|
||||
countries := form.MustGetCountryOptions(r.Context(), conn, user.Locale)
|
||||
|
||||
guest := newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
|
||||
count := guest.count(r)
|
||||
f.Guests = make([]*guestForm, 0, count)
|
||||
guest.FillValueIndex(r, 0)
|
||||
f.Guests = append(f.Guests, guest)
|
||||
for i := 1; i < count; i++ {
|
||||
guest = newGuestFormWithOptions(documentTypes, sexes, countries, "", nil)
|
||||
guest.FillValueIndex(r, i)
|
||||
f.Guests = append(f.Guests, guest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *checkInForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
||||
allOK := true
|
||||
for _, g := range f.Guests {
|
||||
if ok, err := g.Valid(ctx, conn, l); err != nil {
|
||||
return false, err
|
||||
} else if !ok {
|
||||
allOK = false
|
||||
}
|
||||
}
|
||||
return allOK, nil
|
||||
}
|
||||
|
||||
func (f *checkInForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdminFiles(w, r, user, company, f, "booking/checkin.gohtml", "booking/guest.gohtml")
|
||||
}
|
||||
|
||||
type guestForm struct {
|
||||
IDDocumentType *form.Select
|
||||
IDDocumentNumber *form.Input
|
||||
IDDocumentDate *form.Input
|
||||
GivenName *form.Input
|
||||
FirstSurname *form.Input
|
||||
SecondSurname *form.Input
|
||||
Sex *form.Select
|
||||
Birthdate *form.Input
|
||||
Country *form.Select
|
||||
Address *form.Input
|
||||
Phone *form.Input
|
||||
}
|
||||
|
||||
func newGuestForm(ctx context.Context, conn *database.Conn, l *locale.Locale, slug string) (*guestForm, error) {
|
||||
documentTypes := form.MustGetDocumentTypeOptions(ctx, conn, l)
|
||||
sexes := mustGetSexOptions(ctx, conn, l)
|
||||
countries := form.MustGetCountryOptions(ctx, conn, l)
|
||||
|
||||
var address string
|
||||
var country []string
|
||||
row := conn.QueryRow(ctx, "select coalesce(address, ''), array[coalesce(country_code, '')] from booking where slug = $1", slug)
|
||||
if err := row.Scan(&address, &country); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newGuestFormWithOptions(documentTypes, sexes, countries, address, country), nil
|
||||
}
|
||||
|
||||
func newGuestFormWithOptions(documentTypes []*form.Option, sexes []*form.Option, countries []*form.Option, address string, selectedCountry []string) *guestForm {
|
||||
return &guestForm{
|
||||
IDDocumentType: &form.Select{
|
||||
Name: "id_document_type",
|
||||
Options: documentTypes,
|
||||
},
|
||||
IDDocumentNumber: &form.Input{
|
||||
Name: "id_document_number",
|
||||
},
|
||||
IDDocumentDate: &form.Input{
|
||||
Name: "id_document_date",
|
||||
},
|
||||
GivenName: &form.Input{
|
||||
Name: "given_name",
|
||||
},
|
||||
FirstSurname: &form.Input{
|
||||
Name: "first_surname",
|
||||
},
|
||||
SecondSurname: &form.Input{
|
||||
Name: "second_surname",
|
||||
},
|
||||
Sex: &form.Select{
|
||||
Name: "sex",
|
||||
Options: sexes,
|
||||
},
|
||||
Birthdate: &form.Input{
|
||||
Name: "birthdate",
|
||||
},
|
||||
Country: &form.Select{
|
||||
Name: "country",
|
||||
Options: countries,
|
||||
Selected: selectedCountry,
|
||||
},
|
||||
Address: &form.Input{
|
||||
Name: "address",
|
||||
Val: address,
|
||||
},
|
||||
Phone: &form.Input{
|
||||
Name: "phone",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *guestForm) count(r *http.Request) int {
|
||||
keys := []string{f.IDDocumentType.Name, f.IDDocumentNumber.Name, f.IDDocumentDate.Name, f.GivenName.Name, f.FirstSurname.Name, f.SecondSurname.Name, f.Sex.Name, f.Birthdate.Name, f.Country.Name, f.Address.Name, f.Phone.Name}
|
||||
min := math.MaxInt
|
||||
for _, key := range keys {
|
||||
l := len(r.Form[key])
|
||||
if len(r.Form[key]) < min {
|
||||
min = l
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
func (f *guestForm) FillValueIndex(r *http.Request, idx int) {
|
||||
f.IDDocumentType.FillValueIndex(r, idx)
|
||||
f.IDDocumentNumber.FillValueIndex(r, idx)
|
||||
f.IDDocumentDate.FillValueIndex(r, idx)
|
||||
f.GivenName.FillValueIndex(r, idx)
|
||||
f.FirstSurname.FillValueIndex(r, idx)
|
||||
f.SecondSurname.FillValueIndex(r, idx)
|
||||
f.Sex.FillValueIndex(r, idx)
|
||||
f.Birthdate.FillValueIndex(r, idx)
|
||||
f.Country.FillValueIndex(r, idx)
|
||||
f.Address.FillValueIndex(r, idx)
|
||||
f.Phone.FillValueIndex(r, idx)
|
||||
}
|
||||
|
||||
func (f *guestForm) FillFromRow(row pgx.Rows) error {
|
||||
return row.Scan(
|
||||
&f.IDDocumentType.Selected,
|
||||
&f.IDDocumentNumber.Val,
|
||||
&f.IDDocumentDate.Val,
|
||||
&f.GivenName.Val,
|
||||
&f.FirstSurname.Val,
|
||||
&f.SecondSurname.Val,
|
||||
&f.Sex.Selected,
|
||||
&f.Birthdate.Val,
|
||||
&f.Country.Selected,
|
||||
&f.Phone.Val,
|
||||
&f.Address.Val,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *guestForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
||||
v := form.NewValidator(l)
|
||||
|
||||
today := time.Now()
|
||||
yesterday := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
v.CheckSelectedOptions(f.IDDocumentType, l.GettextNoop("Selected ID document type is not valid."))
|
||||
v.CheckRequired(f.IDDocumentNumber, l.GettextNoop("ID document number can not be empty."))
|
||||
if f.IDDocumentDate.Val != "" {
|
||||
if v.CheckValidDate(f.IDDocumentDate, l.GettextNoop("ID document issue date must be a valid date.")) {
|
||||
v.CheckMaxDate(f.IDDocumentDate, yesterday, l.Gettext("ID document issue date must be in the past."))
|
||||
}
|
||||
}
|
||||
v.CheckRequired(f.GivenName, l.GettextNoop("Full name can not be empty."))
|
||||
v.CheckRequired(f.FirstSurname, l.GettextNoop("Full name can not be empty."))
|
||||
v.CheckSelectedOptions(f.Sex, l.GettextNoop("Selected sex is not valid."))
|
||||
if v.CheckRequired(f.Birthdate, l.GettextNoop("Birthdate can not be empty")) {
|
||||
if v.CheckValidDate(f.Birthdate, l.GettextNoop("Birthdate must be a valid date.")) {
|
||||
v.CheckMaxDate(f.Birthdate, yesterday, l.Gettext("Birthdate must be in the past."))
|
||||
}
|
||||
}
|
||||
var country string
|
||||
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
|
||||
country = f.Country.Selected[0]
|
||||
}
|
||||
if f.Phone.Val != "" && country != "" {
|
||||
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return v.AllOK, nil
|
||||
}
|
||||
|
||||
func (f *guestForm) checkedInGuest() *database.CheckedInGuest {
|
||||
birthdate, err := time.Parse(database.ISODateFormat, f.Birthdate.Val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
issueDate, err := time.Parse(database.ISODateFormat, f.IDDocumentDate.Val)
|
||||
if err != nil {
|
||||
issueDate = time.Time{}
|
||||
}
|
||||
return &database.CheckedInGuest{
|
||||
IDDocumentType: f.IDDocumentType.String(),
|
||||
IDDocumentNumber: f.IDDocumentNumber.Val,
|
||||
IDDocumentIssueDate: issueDate,
|
||||
GivenName: f.GivenName.Val,
|
||||
FirstSurname: f.FirstSurname.Val,
|
||||
SecondSurname: f.SecondSurname.Val,
|
||||
Sex: f.Sex.String(),
|
||||
Birthdate: birthdate,
|
||||
CountryCode: f.Country.String(),
|
||||
Phone: f.Phone.Val,
|
||||
Address: f.Address.Val,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *guestForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "booking/guest.gohtml", f)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
)
|
||||
|
||||
type filterForm struct {
|
||||
company *auth.Company
|
||||
HolderName *form.Input
|
||||
BookingStatus *form.Select
|
||||
FromDate *form.Input
|
||||
ToDate *form.Input
|
||||
Cursor *form.Cursor
|
||||
}
|
||||
|
||||
func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm {
|
||||
return &filterForm{
|
||||
company: company,
|
||||
HolderName: &form.Input{
|
||||
Name: "holder_name",
|
||||
},
|
||||
BookingStatus: &form.Select{
|
||||
Name: "booking_status",
|
||||
Options: mustGetBookingStatusOptions(ctx, conn, locale),
|
||||
},
|
||||
FromDate: &form.Input{
|
||||
Name: "from_date",
|
||||
},
|
||||
ToDate: &form.Input{
|
||||
Name: "to_date",
|
||||
},
|
||||
Cursor: &form.Cursor{
|
||||
Name: "cursor",
|
||||
PerPage: 25,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetBookingStatusOptions(ctx context.Context, conn *database.Conn, locale *locale.Locale) []*form.Option {
|
||||
return form.MustGetOptions(ctx, conn, `
|
||||
select booking_status.booking_status
|
||||
, isi18n.name
|
||||
from booking_status
|
||||
join booking_status_i18n isi18n using(booking_status)
|
||||
where isi18n.lang_tag = $1
|
||||
and booking_status <> 'created'
|
||||
order by booking_status`, locale.Language)
|
||||
}
|
||||
|
||||
func (f *filterForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.HolderName.FillValue(r)
|
||||
f.BookingStatus.FillValue(r)
|
||||
f.FromDate.FillValue(r)
|
||||
f.ToDate.FillValue(r)
|
||||
f.Cursor.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||
var where []string
|
||||
appendWhere := func(expression string, value interface{}) {
|
||||
args = append(args, value)
|
||||
where = append(where, fmt.Sprintf(expression, len(args)))
|
||||
}
|
||||
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
||||
if value != "" {
|
||||
if conv == nil {
|
||||
appendWhere(expression, value)
|
||||
} else {
|
||||
appendWhere(expression, conv(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendWhere("booking.company_id = $%d", f.company.ID)
|
||||
maybeAppendWhere("booking.holder_name ILIKE $%d", f.HolderName.Val, func(v string) interface{} {
|
||||
return "%" + v + "%"
|
||||
})
|
||||
if len(f.BookingStatus.Selected) == 0 {
|
||||
where = append(where, "booking.booking_status <> 'created'")
|
||||
} else {
|
||||
maybeAppendWhere("booking.booking_status = $%d", f.BookingStatus.String(), nil)
|
||||
}
|
||||
maybeAppendWhere("lower(stay) >= $%d", f.FromDate.Val, nil)
|
||||
maybeAppendWhere("lower(stay) <= $%d", f.ToDate.Val, nil)
|
||||
|
||||
if f.Paginated() {
|
||||
params := f.Cursor.Params()
|
||||
if len(params) == 2 {
|
||||
where = append(where, fmt.Sprintf("(lower(stay), booking_id) < ($%d, $%d)", len(args)+1, len(args)+2))
|
||||
args = append(args, params[0])
|
||||
args = append(args, params[1])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(where, ") AND ("), args
|
||||
}
|
||||
|
||||
func (f *filterForm) buildCursor(bookings []*bookingEntry) []*bookingEntry {
|
||||
return form.BuildCursor(f.Cursor, bookings, func(entry *bookingEntry) []string {
|
||||
return []string{entry.ArrivalDate.Format(database.ISODateFormat), strconv.Itoa(entry.ID)}
|
||||
})
|
||||
}
|
||||
|
||||
func (f *filterForm) HasValue() bool {
|
||||
return f.HolderName.Val != "" ||
|
||||
(len(f.BookingStatus.Selected) > 0 && f.BookingStatus.Selected[0] != "") ||
|
||||
f.FromDate.Val != "" ||
|
||||
f.ToDate.Val != ""
|
||||
}
|
||||
|
||||
func (f *filterForm) PerPage() int {
|
||||
return f.Cursor.PerPage
|
||||
}
|
||||
|
||||
func (f *filterForm) Paginated() bool {
|
||||
return f.Cursor.Pagination
|
||||
}
|
|
@ -43,7 +43,6 @@ func requestPayment(w http.ResponseWriter, r *http.Request, user *auth.User, com
|
|||
f.Customer.Email.Val,
|
||||
f.Customer.Phone.Val,
|
||||
user.Locale.Language,
|
||||
f.Customer.ACSICard.Checked,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package booking
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type PrebookingHandler struct {
|
||||
}
|
||||
|
||||
func NewPrebookingHandler() *PrebookingHandler {
|
||||
return &PrebookingHandler{}
|
||||
}
|
||||
|
||||
func (h *PrebookingHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
servePrebookingIndex(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func servePrebookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
filters := newFilterForm(r.Context(), conn, company, user.Locale)
|
||||
if err := filters.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
filters.BookingStatus.Selected = []string{"created"}
|
||||
bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language, filters)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
page := &prebookingIndex{
|
||||
Bookings: filters.buildCursor(bookings),
|
||||
Filters: filters,
|
||||
}
|
||||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
type prebookingIndex struct {
|
||||
Bookings []*bookingEntry
|
||||
Filters *filterForm
|
||||
}
|
||||
|
||||
func (page prebookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "prebooking/results.gohtml", page)
|
||||
} else {
|
||||
template.MustRenderAdminFiles(w, r, user, company, page, "prebooking/index.gohtml", "prebooking/results.gohtml")
|
||||
}
|
||||
}
|
|
@ -119,6 +119,7 @@ type bookingGuestFields struct {
|
|||
NumberTeenagers *form.Input
|
||||
NumberChildren *form.Input
|
||||
NumberDogs *form.Input
|
||||
ACSICard *form.Checkbox
|
||||
Error error
|
||||
}
|
||||
|
||||
|
@ -129,11 +130,12 @@ type bookingOptionFields struct {
|
|||
}
|
||||
|
||||
type campsiteTypeOption struct {
|
||||
ID int
|
||||
Label string
|
||||
Min int
|
||||
Max int
|
||||
Input *form.Input
|
||||
ID int
|
||||
Label string
|
||||
Min int
|
||||
Max int
|
||||
Input *form.Input
|
||||
Subtotal string
|
||||
}
|
||||
|
||||
type bookingCustomerFields struct {
|
||||
|
@ -144,24 +146,27 @@ type bookingCustomerFields struct {
|
|||
Country *form.Select
|
||||
Email *form.Input
|
||||
Phone *form.Input
|
||||
ACSICard *form.Checkbox
|
||||
Agreement *form.Checkbox
|
||||
}
|
||||
|
||||
func newEmptyBookingForm(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) *bookingForm {
|
||||
return &bookingForm{
|
||||
CampsiteType: &form.Select{
|
||||
Name: "campsite_type",
|
||||
Options: form.MustGetOptions(ctx, conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 and active order by position, l10n_name", l.Language, company.ID),
|
||||
},
|
||||
PaymentSlug: &form.Input{
|
||||
Name: "payment_slug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newBookingForm(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*bookingForm, error) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &bookingForm{
|
||||
CampsiteType: &form.Select{
|
||||
Name: "campsite_type",
|
||||
Options: form.MustGetOptions(r.Context(), conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 and active order by position, l10n_name", l.Language, company.ID),
|
||||
},
|
||||
PaymentSlug: &form.Input{
|
||||
Name: "payment_slug",
|
||||
},
|
||||
}
|
||||
f := newEmptyBookingForm(r.Context(), conn, company, l)
|
||||
f.CampsiteType.FillValue(r)
|
||||
f.PaymentSlug.FillValue(r)
|
||||
campsiteType := f.CampsiteType.String()
|
||||
|
@ -183,7 +188,7 @@ func newBookingForm(r *http.Request, company *auth.Company, conn *database.Conn,
|
|||
return f, nil
|
||||
}
|
||||
|
||||
f.Guests, err = newBookingGuestFields(r.Context(), conn, campsiteType)
|
||||
f.Guests, err = newBookingGuestFields(r.Context(), conn, campsiteType, f.Dates.ArrivalDate.Val, f.Dates.DepartureDate.Val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -240,14 +245,14 @@ func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string
|
|||
row := conn.QueryRow(ctx, `
|
||||
select lower(bookable_nights),
|
||||
upper(bookable_nights) - 1,
|
||||
greatest(min(lower(season_range)), current_timestamp::date),
|
||||
max(upper(season_range))
|
||||
greatest(min(lower(season_range)), lower(operating_dates), current_timestamp::date),
|
||||
least(max(upper(season_range)), upper(operating_dates))
|
||||
from campsite_type
|
||||
join campsite_type_cost using (campsite_type_id)
|
||||
join season_calendar using (season_id)
|
||||
where campsite_type.slug = $1
|
||||
and season_range >> daterange(date_trunc('year', current_timestamp)::date, date_trunc('year', current_timestamp)::date + 1)
|
||||
group by bookable_nights;
|
||||
group by bookable_nights, operating_dates
|
||||
`, campsiteType)
|
||||
f := &DateFields{
|
||||
ArrivalDate: &bookingDateInput{
|
||||
|
@ -269,7 +274,10 @@ func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string
|
|||
func (f *DateFields) FillValues(r *http.Request, l *locale.Locale) {
|
||||
f.ArrivalDate.FillValue(r)
|
||||
f.DepartureDate.FillValue(r)
|
||||
f.AdjustValues(l)
|
||||
}
|
||||
|
||||
func (f *DateFields) AdjustValues(l *locale.Locale) {
|
||||
if f.ArrivalDate.Val != "" {
|
||||
arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
|
||||
if err != nil {
|
||||
|
@ -317,7 +325,7 @@ func (f *DateFields) Valid(v *form.Validator, l *locale.Locale) {
|
|||
}
|
||||
}
|
||||
|
||||
func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteType string) (*bookingGuestFields, error) {
|
||||
func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteType string, arrivalDate string, departureDate string) (*bookingGuestFields, error) {
|
||||
f := &bookingGuestFields{
|
||||
NumberAdults: &form.Input{Name: "number_adults"},
|
||||
NumberTeenagers: &form.Input{Name: "number_teenagers"},
|
||||
|
@ -327,17 +335,26 @@ func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteTyp
|
|||
select max_campers
|
||||
, overflow_allowed
|
||||
, pet.cost_per_night is not null as dogs_allowed
|
||||
, exists (
|
||||
select 1 from acsi_calendar
|
||||
where acsi_calendar.campsite_type_id = campsite_type.campsite_type_id
|
||||
and acsi_range && daterange($2::date, $3::date)
|
||||
) as acsi_allowed
|
||||
from campsite_type
|
||||
left join campsite_type_pet_cost as pet using (campsite_type_id)
|
||||
where slug = $1
|
||||
`, campsiteType)
|
||||
`, campsiteType, arrivalDate, departureDate)
|
||||
var dogsAllowed bool
|
||||
if err := row.Scan(&f.MaxGuests, &f.OverflowAllowed, &dogsAllowed); err != nil {
|
||||
var ACSIAllowed bool
|
||||
if err := row.Scan(&f.MaxGuests, &f.OverflowAllowed, &dogsAllowed, &ACSIAllowed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dogsAllowed {
|
||||
f.NumberDogs = &form.Input{Name: "number_dogs"}
|
||||
}
|
||||
if ACSIAllowed {
|
||||
f.ACSICard = &form.Checkbox{Name: "acsi_card"}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
|
@ -349,6 +366,13 @@ func (f *bookingGuestFields) FillValues(r *http.Request, l *locale.Locale) {
|
|||
if f.NumberDogs != nil {
|
||||
fillNumericField(f.NumberDogs, r, 0)
|
||||
}
|
||||
if f.ACSICard != nil {
|
||||
f.ACSICard.FillValue(r)
|
||||
}
|
||||
f.AdjustValues(numGuests, l)
|
||||
}
|
||||
|
||||
func (f *bookingGuestFields) AdjustValues(numGuests int, l *locale.Locale) {
|
||||
if numGuests > f.MaxGuests {
|
||||
if f.OverflowAllowed {
|
||||
f.Overflow = true
|
||||
|
@ -459,6 +483,42 @@ func (f *bookingOptionFields) FillValues(r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *bookingOptionFields) FillFromDatabase(ctx context.Context, conn *database.Conn, bookingID int) error {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite_type_option.campsite_type_option_id
|
||||
, coalesce(units, lower(range))::text
|
||||
, to_price(coalesce(subtotal, 0), decimal_digits)
|
||||
from booking
|
||||
join campsite_type_option using (campsite_type_id)
|
||||
left join booking_option
|
||||
on booking.booking_id = booking_option.booking_id
|
||||
and booking_option.campsite_type_option_id = campsite_type_option.campsite_type_option_id
|
||||
join currency using (currency_code)
|
||||
where booking.booking_id = $1
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var units string
|
||||
var subtotal string
|
||||
if err = rows.Scan(&id, &units, &subtotal); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, option := range f.Options {
|
||||
if option.ID == id {
|
||||
option.Input.Val = units
|
||||
option.Subtotal = subtotal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *bookingOptionFields) Valid(v *form.Validator, l *locale.Locale) {
|
||||
for _, option := range f.Options {
|
||||
if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) {
|
||||
|
@ -487,7 +547,7 @@ func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *local
|
|||
},
|
||||
Country: &form.Select{
|
||||
Name: "country",
|
||||
Options: form.MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language),
|
||||
Options: form.MustGetCountryOptions(ctx, conn, l),
|
||||
},
|
||||
Email: &form.Input{
|
||||
Name: "email",
|
||||
|
@ -495,9 +555,6 @@ func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *local
|
|||
Phone: &form.Input{
|
||||
Name: "phone",
|
||||
},
|
||||
ACSICard: &form.Checkbox{
|
||||
Name: "acsi_card",
|
||||
},
|
||||
Agreement: &form.Checkbox{
|
||||
Name: "agreement",
|
||||
},
|
||||
|
@ -512,7 +569,6 @@ func (f *bookingCustomerFields) FillValues(r *http.Request) {
|
|||
f.Country.FillValue(r)
|
||||
f.Email.FillValue(r)
|
||||
f.Phone.FillValue(r)
|
||||
f.ACSICard.FillValue(r)
|
||||
f.Agreement.FillValue(r)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package build
|
||||
|
||||
const Version = "1.5~git"
|
||||
const Version = "1.8~git"
|
||||
|
|
|
@ -8,10 +8,12 @@ package campsite
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/booking"
|
||||
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
|
@ -88,54 +90,57 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
|||
}
|
||||
|
||||
func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
campsites, err := collectCampsiteEntries(r.Context(), company, conn)
|
||||
page := newCampsiteIndex()
|
||||
if err := page.Parse(r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var err error
|
||||
from := page.From.Date()
|
||||
to := page.To.Date().AddDate(0, 1, 0)
|
||||
page.Campsites, err = booking.CollectCampsiteEntries(r.Context(), company, conn, from, to, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
page := &campsiteIndex{
|
||||
Campsites: campsites,
|
||||
}
|
||||
page.Months = booking.CollectMonths(from, to)
|
||||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*campsiteEntry, error) {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select campsite.label
|
||||
, campsite_type.name
|
||||
, campsite.active
|
||||
from campsite
|
||||
join campsite_type using (campsite_type_id)
|
||||
where campsite.company_id = $1
|
||||
order by label`, company.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var campsites []*campsiteEntry
|
||||
for rows.Next() {
|
||||
entry := &campsiteEntry{}
|
||||
if err = rows.Scan(&entry.Label, &entry.Type, &entry.Active); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
campsites = append(campsites, entry)
|
||||
}
|
||||
|
||||
return campsites, nil
|
||||
}
|
||||
|
||||
type campsiteEntry struct {
|
||||
Label string
|
||||
Type string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type campsiteIndex struct {
|
||||
Campsites []*campsiteEntry
|
||||
From *form.Month
|
||||
To *form.Month
|
||||
Campsites []*booking.CampsiteEntry
|
||||
Months []*booking.Month
|
||||
}
|
||||
|
||||
func newCampsiteIndex() *campsiteIndex {
|
||||
now := time.Now()
|
||||
from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
to := from.AddDate(0, 3, 0)
|
||||
return &campsiteIndex{
|
||||
From: &form.Month{
|
||||
Name: "from",
|
||||
Year: from.Year(),
|
||||
Month: from.Month(),
|
||||
},
|
||||
To: &form.Month{
|
||||
Name: "to",
|
||||
Year: to.Year(),
|
||||
Month: to.Month(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (page *campsiteIndex) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
page.From.FillValue(r)
|
||||
page.To.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "web/templates/campground_map.svg")
|
||||
template.MustRenderAdminFiles(w, r, user, company, page, "campsite/index.gohtml", "booking/grid.gohtml")
|
||||
}
|
||||
|
||||
func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
|
|
|
@ -0,0 +1,403 @@
|
|||
package customer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
}
|
||||
|
||||
func NewAdminHandler() *AdminHandler {
|
||||
return &AdminHandler{}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveCustomerIndex(w, r, user, company, conn)
|
||||
case http.MethodPost:
|
||||
addCustomer(w, r, user, company, conn)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||
}
|
||||
case "new":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
f := NewContactForm(r.Context(), conn, user.Locale)
|
||||
f.MustRender(w, r, user, company)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
f := NewContactForm(r.Context(), conn, user.Locale)
|
||||
if err := f.FillFromDatabase(r.Context(), conn, head); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
h.customerHandler(user, company, conn, f).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) customerHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
f.MustRender(w, r, user, company)
|
||||
case http.MethodPut:
|
||||
editCustomer(w, r, user, company, conn, f)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func serveCustomerIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
filters := newFilterForm(company)
|
||||
if err := filters.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
customers, err := collectCustomerEntries(r.Context(), conn, company, filters)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
page := &customerIndex{
|
||||
Customers: filters.buildCursor(customers),
|
||||
Filters: filters,
|
||||
}
|
||||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *auth.Company, filters *filterForm) ([]*customerEntry, error) {
|
||||
where, args := filters.BuildQuery(nil)
|
||||
rows, err := conn.Query(ctx, fmt.Sprintf(`
|
||||
select contact_id
|
||||
, '/admin/customers/' || slug
|
||||
, name
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
from contact
|
||||
left join contact_email using (contact_id)
|
||||
left join contact_phone using (contact_id)
|
||||
where (%s)
|
||||
order by name, contact_id
|
||||
LIMIT %d
|
||||
`, where, filters.PerPage()+1), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var customers []*customerEntry
|
||||
for rows.Next() {
|
||||
customer := &customerEntry{}
|
||||
if err = rows.Scan(&customer.ID, &customer.URL, &customer.Name, &customer.Email, &customer.Phone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customers = append(customers, customer)
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
type customerEntry struct {
|
||||
ID int
|
||||
URL string
|
||||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
}
|
||||
|
||||
type customerIndex struct {
|
||||
Customers []*customerEntry
|
||||
Filters *filterForm
|
||||
}
|
||||
|
||||
func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "customer/results.gohtml", page)
|
||||
} else {
|
||||
template.MustRenderAdminFiles(w, r, user, company, page, "customer/index.gohtml", "customer/results.gohtml")
|
||||
}
|
||||
}
|
||||
|
||||
func addCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
f := NewContactForm(r.Context(), conn, user.Locale)
|
||||
processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
|
||||
var err error
|
||||
f.Slug, err = tx.AddContact(ctx, company.ID, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func editCustomer(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm) {
|
||||
processCustomerForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
|
||||
_, err := tx.EditContact(ctx, f.Slug, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func processCustomerForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *ContactForm, act func(ctx context.Context, tx *database.Tx) error) {
|
||||
if err := f.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := user.VerifyCSRFToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
|
||||
panic(err)
|
||||
} else if !ok {
|
||||
if !httplib.IsHTMxRequest(r) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
f.MustRender(w, r, user, company)
|
||||
return
|
||||
}
|
||||
|
||||
tx := conn.MustBegin(r.Context())
|
||||
if err := act(r.Context(), tx); err == nil {
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Rollback(r.Context()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
httplib.Redirect(w, r, "/admin/customers", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type ContactForm struct {
|
||||
URL string
|
||||
Slug string
|
||||
FullName *form.Input
|
||||
IDDocumentType *form.Select
|
||||
IDDocumentNumber *form.Input
|
||||
Address *form.Input
|
||||
City *form.Input
|
||||
Province *form.Input
|
||||
PostalCode *form.Input
|
||||
Country *form.Select
|
||||
Email *form.Input
|
||||
Phone *form.Input
|
||||
}
|
||||
|
||||
func NewContactForm(ctx context.Context, conn *database.Conn, l *locale.Locale) *ContactForm {
|
||||
return &ContactForm{
|
||||
FullName: &form.Input{
|
||||
Name: "full_name",
|
||||
},
|
||||
IDDocumentType: &form.Select{
|
||||
Name: "id_document_type",
|
||||
Options: form.MustGetDocumentTypeOptions(ctx, conn, l),
|
||||
},
|
||||
IDDocumentNumber: &form.Input{
|
||||
Name: "id_document_number",
|
||||
},
|
||||
Address: &form.Input{
|
||||
Name: "address",
|
||||
},
|
||||
City: &form.Input{
|
||||
Name: "city",
|
||||
},
|
||||
Province: &form.Input{
|
||||
Name: "province",
|
||||
},
|
||||
PostalCode: &form.Input{
|
||||
Name: "postal_code",
|
||||
},
|
||||
Country: &form.Select{
|
||||
Name: "country",
|
||||
Options: form.MustGetCountryOptions(ctx, conn, l),
|
||||
},
|
||||
Email: &form.Input{
|
||||
Name: "email",
|
||||
},
|
||||
Phone: &form.Input{
|
||||
Name: "phone",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ContactForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
|
||||
row := conn.QueryRow(ctx, `
|
||||
select '/admin/customers/' || slug
|
||||
, slug
|
||||
, name
|
||||
, array[id_document_type_id::text]
|
||||
, id_document_number
|
||||
, address
|
||||
, city
|
||||
, province
|
||||
, postal_code
|
||||
, array[country_code::text]
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
from contact as text
|
||||
left join contact_email using (contact_id)
|
||||
left join contact_phone using (contact_id)
|
||||
where slug = $1
|
||||
`, slug)
|
||||
return row.Scan(
|
||||
&f.URL,
|
||||
&f.Slug,
|
||||
&f.FullName.Val,
|
||||
&f.IDDocumentType.Selected,
|
||||
&f.IDDocumentNumber.Val,
|
||||
&f.Address.Val,
|
||||
&f.City.Val,
|
||||
&f.Province.Val,
|
||||
&f.PostalCode.Val,
|
||||
&f.Country.Selected,
|
||||
&f.Email.Val,
|
||||
&f.Phone.Val,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *ContactForm) FillFromBooking(ctx context.Context, conn *database.Conn, bookingID int) error {
|
||||
row := conn.QueryRow(ctx, `
|
||||
select ''
|
||||
, ''
|
||||
, holder_name
|
||||
, array[]::text[]
|
||||
, ''
|
||||
, coalesce(address, '')
|
||||
, coalesce(city, '')
|
||||
, ''
|
||||
, coalesce(postal_code, '')
|
||||
, array[coalesce(country_code::text, '')]
|
||||
, coalesce(email::text, '')
|
||||
, coalesce(phone::text, '')
|
||||
from booking
|
||||
where booking_id = $1
|
||||
`, bookingID)
|
||||
return row.Scan(
|
||||
&f.URL,
|
||||
&f.Slug,
|
||||
&f.FullName.Val,
|
||||
&f.IDDocumentType.Selected,
|
||||
&f.IDDocumentNumber.Val,
|
||||
&f.Address.Val,
|
||||
&f.City.Val,
|
||||
&f.Province.Val,
|
||||
&f.PostalCode.Val,
|
||||
&f.Country.Selected,
|
||||
&f.Email.Val,
|
||||
&f.Phone.Val,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *ContactForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.FullName.FillValue(r)
|
||||
f.IDDocumentType.FillValue(r)
|
||||
f.IDDocumentNumber.FillValue(r)
|
||||
f.Address.FillValue(r)
|
||||
f.City.FillValue(r)
|
||||
f.Province.FillValue(r)
|
||||
f.PostalCode.FillValue(r)
|
||||
f.Country.FillValue(r)
|
||||
f.Email.FillValue(r)
|
||||
f.Phone.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ContactForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
|
||||
v := form.NewValidator(l)
|
||||
|
||||
var country string
|
||||
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
|
||||
country = f.Country.Selected[0]
|
||||
}
|
||||
|
||||
v.CheckSelectedOptions(f.IDDocumentType, l.GettextNoop("Selected ID document type is not valid."))
|
||||
v.CheckRequired(f.IDDocumentNumber, l.GettextNoop("ID document number can not be empty."))
|
||||
|
||||
if v.CheckRequired(f.FullName, l.GettextNoop("Full name can not be empty.")) {
|
||||
v.CheckMinLength(f.FullName, 1, l.GettextNoop("Full name must have at least one letter."))
|
||||
}
|
||||
|
||||
v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty."))
|
||||
v.CheckRequired(f.City, l.GettextNoop("Town or village can not be empty."))
|
||||
if v.CheckRequired(f.PostalCode, l.GettextNoop("Postcode can not be empty.")) && country != "" {
|
||||
if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if f.Email.Val != "" {
|
||||
v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
|
||||
}
|
||||
if f.Phone.Val != "" && country != "" {
|
||||
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return v.AllOK, nil
|
||||
}
|
||||
|
||||
func (f *ContactForm) UpdateOrCreate(ctx context.Context, company *auth.Company, tx *database.Tx) (int, error) {
|
||||
var contactID int
|
||||
row := tx.QueryRow(ctx, `
|
||||
select contact_id, slug from contact where id_document_type_id = $1 and id_document_number = $2 and country_code = $3
|
||||
`,
|
||||
f.IDDocumentType.String(),
|
||||
f.IDDocumentNumber.Val,
|
||||
f.Country.String(),
|
||||
)
|
||||
if err := row.Scan(&contactID, &f.Slug); err != nil {
|
||||
if database.ErrorIsNotFound(err) {
|
||||
f.Slug, err = tx.AddContact(ctx, company.ID, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
contactID, err = tx.GetInt(ctx, "select contact_id from contact where slug = $1", f.Slug)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
_, err := tx.EditContact(ctx, f.Slug, f.FullName.Val, f.IDDocumentType.String(), f.IDDocumentNumber.Val, f.Phone.Val, f.Email.Val, f.Address.Val, f.City.Val, f.Province.Val, f.PostalCode.Val, f.Country.String())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return contactID, nil
|
||||
}
|
||||
|
||||
func (f *ContactForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderAdminFiles(w, r, user, company, f, "customer/form.gohtml", "customer/contact.gohtml")
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package customer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
)
|
||||
|
||||
type filterForm struct {
|
||||
company *auth.Company
|
||||
Name *form.Input
|
||||
Email *form.Input
|
||||
Cursor *form.Cursor
|
||||
}
|
||||
|
||||
func newFilterForm(company *auth.Company) *filterForm {
|
||||
return &filterForm{
|
||||
company: company,
|
||||
Name: &form.Input{
|
||||
Name: "name",
|
||||
},
|
||||
Email: &form.Input{
|
||||
Name: "email",
|
||||
},
|
||||
Cursor: &form.Cursor{
|
||||
Name: "cursor",
|
||||
PerPage: 25,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *filterForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Name.FillValue(r)
|
||||
f.Email.FillValue(r)
|
||||
f.Cursor.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||
var where []string
|
||||
appendWhere := func(expression string, value interface{}) {
|
||||
args = append(args, value)
|
||||
where = append(where, fmt.Sprintf(expression, len(args)))
|
||||
}
|
||||
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
||||
if value != "" {
|
||||
if conv == nil {
|
||||
appendWhere(expression, value)
|
||||
} else {
|
||||
appendWhere(expression, conv(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendWhere("company_id = $%d", f.company.ID)
|
||||
maybeAppendWhere("name ILIKE $%d", f.Name.Val, func(v string) interface{} {
|
||||
return "%" + v + "%"
|
||||
})
|
||||
maybeAppendWhere("email ILIKE $%d", f.Email.Val, func(v string) interface{} {
|
||||
return "%" + v + "%"
|
||||
})
|
||||
|
||||
if f.Paginated() {
|
||||
params := f.Cursor.Params()
|
||||
if len(params) == 2 {
|
||||
where = append(where, fmt.Sprintf("(name, contact_id) > ($%d, $%d)", len(args)+1, len(args)+2))
|
||||
args = append(args, params[0])
|
||||
args = append(args, params[1])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(where, ") AND ("), args
|
||||
}
|
||||
|
||||
func (f *filterForm) buildCursor(customers []*customerEntry) []*customerEntry {
|
||||
return form.BuildCursor(f.Cursor, customers, func(entry *customerEntry) []string {
|
||||
return []string{entry.Name, strconv.Itoa(entry.ID)}
|
||||
})
|
||||
}
|
||||
|
||||
func (f *filterForm) HasValue() bool {
|
||||
return f.Name.Val != "" ||
|
||||
f.Email.Val != ""
|
||||
}
|
||||
|
||||
func (f *filterForm) PerPage() int {
|
||||
return f.Cursor.PerPage
|
||||
}
|
||||
|
||||
func (f *filterForm) Paginated() bool {
|
||||
return f.Cursor.Pagination
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgio"
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
type CheckedInGuest struct {
|
||||
IDDocumentType string
|
||||
IDDocumentNumber string
|
||||
IDDocumentIssueDate time.Time
|
||||
GivenName string
|
||||
FirstSurname string
|
||||
SecondSurname string
|
||||
Sex string
|
||||
Birthdate time.Time
|
||||
CountryCode string
|
||||
Phone string
|
||||
Address string
|
||||
}
|
||||
|
||||
func (src CheckedInGuest) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) {
|
||||
typeName := CheckedInGuestTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var idDocumentIssueDate interface{}
|
||||
var noDate time.Time
|
||||
if src.IDDocumentIssueDate != noDate {
|
||||
idDocumentIssueDate = src.IDDocumentIssueDate
|
||||
}
|
||||
values := []interface{}{
|
||||
src.IDDocumentType,
|
||||
src.IDDocumentNumber,
|
||||
idDocumentIssueDate,
|
||||
src.GivenName,
|
||||
src.FirstSurname,
|
||||
src.SecondSurname,
|
||||
src.Sex,
|
||||
src.Birthdate,
|
||||
src.CountryCode,
|
||||
src.Phone,
|
||||
src.Address,
|
||||
}
|
||||
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
|
||||
if err := ct.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ct.EncodeBinary(ci, dst)
|
||||
}
|
||||
|
||||
type CheckedInGuestArray []*CheckedInGuest
|
||||
|
||||
func (src CheckedInGuestArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := CheckedInGuestTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
|
||||
arrayHeader := pgtype.ArrayHeader{
|
||||
ElementOID: int32(dt.OID),
|
||||
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(src)), LowerBound: 1}},
|
||||
}
|
||||
buf = arrayHeader.EncodeBinary(ci, buf)
|
||||
for _, optionUnits := range src {
|
||||
sp := len(buf)
|
||||
buf = pgio.AppendInt32(buf, -1)
|
||||
|
||||
elemBuf, err := optionUnits.EncodeBinary(ci, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if elemBuf != nil {
|
||||
buf = elemBuf
|
||||
pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4))
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgio"
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
const EditedInvoiceProductTypeName = "edited_invoice_product"
|
||||
|
||||
type EditedInvoiceProduct struct {
|
||||
*NewInvoiceProduct
|
||||
InvoiceProductId int
|
||||
}
|
||||
|
||||
func (src EditedInvoiceProduct) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) {
|
||||
typeName := EditedInvoiceProductTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var invoiceProductId interface{}
|
||||
if src.InvoiceProductId > 0 {
|
||||
invoiceProductId = src.InvoiceProductId
|
||||
}
|
||||
var productId interface{}
|
||||
if src.ProductId > 0 {
|
||||
productId = src.ProductId
|
||||
}
|
||||
values := []interface{}{
|
||||
invoiceProductId,
|
||||
productId,
|
||||
src.Name,
|
||||
src.Description,
|
||||
src.Price,
|
||||
src.Quantity,
|
||||
src.Discount,
|
||||
src.Taxes,
|
||||
}
|
||||
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
|
||||
if err := ct.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ct.EncodeBinary(ci, dst)
|
||||
}
|
||||
|
||||
type EditedInvoiceProductArray []*EditedInvoiceProduct
|
||||
|
||||
func (src EditedInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := EditedInvoiceProductTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
|
||||
arrayHeader := pgtype.ArrayHeader{
|
||||
ElementOID: int32(dt.OID),
|
||||
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(src)), LowerBound: 1}},
|
||||
}
|
||||
buf = arrayHeader.EncodeBinary(ci, buf)
|
||||
for _, product := range src {
|
||||
sp := len(buf)
|
||||
buf = pgio.AppendInt32(buf, -1)
|
||||
|
||||
elemBuf, err := product.EncodeBinary(ci, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if elemBuf != nil {
|
||||
buf = elemBuf
|
||||
pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4))
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgio"
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
const NewInvoiceProductTypeName = "new_invoice_product"
|
||||
|
||||
type NewInvoiceProduct struct {
|
||||
ProductId int
|
||||
Name string
|
||||
Description string
|
||||
Price string
|
||||
Quantity int
|
||||
Discount float64
|
||||
Taxes []int
|
||||
}
|
||||
|
||||
func (src NewInvoiceProduct) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) {
|
||||
typeName := NewInvoiceProductTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
var productId interface{}
|
||||
if src.ProductId > 0 {
|
||||
productId = src.ProductId
|
||||
}
|
||||
values := []interface{}{
|
||||
productId,
|
||||
src.Name,
|
||||
src.Description,
|
||||
src.Price,
|
||||
src.Quantity,
|
||||
src.Discount,
|
||||
src.Taxes,
|
||||
}
|
||||
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
|
||||
if err := ct.Set(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ct.EncodeBinary(ci, dst)
|
||||
}
|
||||
|
||||
type NewInvoiceProductArray []*NewInvoiceProduct
|
||||
|
||||
func (src NewInvoiceProductArray) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
|
||||
typeName := NewInvoiceProductTypeName
|
||||
dt, ok := ci.DataTypeForName(typeName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||
}
|
||||
|
||||
arrayHeader := pgtype.ArrayHeader{
|
||||
ElementOID: int32(dt.OID),
|
||||
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(src)), LowerBound: 1}},
|
||||
}
|
||||
buf = arrayHeader.EncodeBinary(ci, buf)
|
||||
for _, product := range src {
|
||||
sp := len(buf)
|
||||
buf = pgio.AppendInt32(buf, -1)
|
||||
|
||||
elemBuf, err := product.EncodeBinary(ci, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if elemBuf != nil {
|
||||
buf = elemBuf
|
||||
pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4))
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
|
@ -7,6 +7,7 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jackc/pgtype/zeronull"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
|
@ -348,6 +349,45 @@ func (tx *Tx) TranslateHome(ctx context.Context, companyID int, langTag language
|
|||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) ReadyPayment(ctx context.Context, paymentSlug string, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag, acsiCard bool) (int, error) {
|
||||
return c.GetInt(ctx, "select ready_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", paymentSlug, customerName, customerAddress, customerPostCode, customerCity, customerCountryCode, customerEmail, customerPhone, customerLangTag, acsiCard)
|
||||
func (c *Conn) ReadyPayment(ctx context.Context, paymentSlug string, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag) (int, error) {
|
||||
return c.GetInt(ctx, "select ready_payment($1, $2, $3, $4, $5, $6, $7, $8, $9)", paymentSlug, customerName, customerAddress, customerPostCode, customerCity, customerCountryCode, customerEmail, customerPhone, customerLangTag)
|
||||
}
|
||||
|
||||
func (tx *Tx) AddBookingFromPayment(ctx context.Context, paymentSlug string) (int, error) {
|
||||
return tx.GetInt(ctx, "select add_booking_from_payment($1)", paymentSlug)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditBookingFromPayment(ctx context.Context, bookingSlug string, paymentSlug string) (int, error) {
|
||||
return tx.GetInt(ctx, "select edit_booking_from_payment($1, $2)", bookingSlug, paymentSlug)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditBooking(ctx context.Context, bookingID int, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag, bookingStatus string, campsiteIDs []int) error {
|
||||
_, err := tx.Exec(ctx, "select edit_booking($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", bookingID, customerName, zeronull.Text(customerAddress), zeronull.Text(customerPostCode), zeronull.Text(customerCity), zeronull.Text(customerCountryCode), zeronull.Text(customerEmail), zeronull.Text(customerPhone), customerLangTag, bookingStatus, campsiteIDs)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) CancelBooking(ctx context.Context, bookingID int) error {
|
||||
_, err := c.Exec(ctx, "select cancel_booking($1)", bookingID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) CheckInGuests(ctx context.Context, bookingSlug string, guests []*CheckedInGuest) error {
|
||||
_, err := c.Exec(ctx, "select check_in_guests(booking_id, $2) from booking where slug = $1", bookingSlug, CheckedInGuestArray(guests))
|
||||
return err
|
||||
}
|
||||
|
||||
func (tx *Tx) AddInvoice(ctx context.Context, companyID int, date string, customerID int, notes string, paymentMethodID int, products NewInvoiceProductArray) (string, error) {
|
||||
return tx.GetText(ctx, "select add_invoice($1, $2, $3, $4, $5, $6)", companyID, date, customerID, notes, paymentMethodID, products)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditInvoice(ctx context.Context, invoiceSlug string, invoiceStatus string, contactID int, notes string, paymentMethodID int, products EditedInvoiceProductArray) (string, error) {
|
||||
return tx.GetText(ctx, "select edit_invoice($1, $2, $3, $4, $5, $6)", invoiceSlug, invoiceStatus, contactID, notes, paymentMethodID, products)
|
||||
}
|
||||
|
||||
func (tx *Tx) AddContact(ctx context.Context, companyID int, name string, idDocumentType string, idDocumentNumber string, phone string, email string, address string, city string, province string, postalCode string, countryCode string) (string, error) {
|
||||
return tx.GetText(ctx, "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", companyID, name, idDocumentType, idDocumentNumber, phone, email, address, city, province, postalCode, countryCode)
|
||||
}
|
||||
|
||||
func (tx *Tx) EditContact(ctx context.Context, contactSlug, name string, idDocumentType string, idDocumentNumber string, phone string, email string, address string, city string, province string, postalCode string, countryCode string) (string, error) {
|
||||
return tx.GetText(ctx, "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", contactSlug, name, idDocumentType, idDocumentNumber, phone, email, address, city, province, postalCode, countryCode)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
CheckedInGuestTypeName = "checked_in_guest"
|
||||
OptionUnitsTypeName = "option_units"
|
||||
RedsysRequestTypeName = "redsys_request"
|
||||
RedsysResponseTypeName = "redsys_response"
|
||||
|
@ -48,6 +49,11 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
|
|||
return err
|
||||
}
|
||||
|
||||
discountRateOID, err := registerType(ctx, conn, &pgtype.Numeric{}, "discount_rate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
redsysRequestType, err := pgtype.NewCompositeType(
|
||||
RedsysRequestTypeName,
|
||||
[]pgtype.CompositeTypeField{
|
||||
|
@ -125,6 +131,71 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
|
|||
return err
|
||||
}
|
||||
|
||||
checkedInGuestType, err := pgtype.NewCompositeType(
|
||||
CheckedInGuestTypeName,
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"id_document_type_id", pgtype.VarcharOID},
|
||||
{"id_document_number", pgtype.TextOID},
|
||||
{"id_document_issue_date", pgtype.DateOID},
|
||||
{"given_name", pgtype.TextOID},
|
||||
{"first_surname", pgtype.TextOID},
|
||||
{"second_surname", pgtype.TextOID},
|
||||
{"sex_id", pgtype.VarcharOID},
|
||||
{"birthdate", pgtype.DateOID},
|
||||
{"country_code", pgtype.TextOID},
|
||||
{"phone", pgtype.TextOID},
|
||||
{"address", pgtype.TextOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = registerType(ctx, conn, checkedInGuestType, checkedInGuestType.TypeName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newInvoiceProductType, err := pgtype.NewCompositeType(
|
||||
NewInvoiceProductTypeName,
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"product_id", pgtype.Int4OID},
|
||||
{"name", pgtype.TextOID},
|
||||
{"description", pgtype.TextOID},
|
||||
{"price", pgtype.TextOID},
|
||||
{"quantity", pgtype.Int4OID},
|
||||
{"discount_rate", discountRateOID},
|
||||
{"tax", pgtype.Int4ArrayOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = registerType(ctx, conn, newInvoiceProductType, newInvoiceProductType.TypeName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
editedInvoiceProductType, err := pgtype.NewCompositeType(
|
||||
EditedInvoiceProductTypeName,
|
||||
[]pgtype.CompositeTypeField{
|
||||
{"invoice_product_id", pgtype.Int4OID},
|
||||
{"product_id", pgtype.Int4OID},
|
||||
{"name", pgtype.TextOID},
|
||||
{"description", pgtype.TextOID},
|
||||
{"price", pgtype.TextOID},
|
||||
{"quantity", pgtype.Int4OID},
|
||||
{"discount_rate", discountRateOID},
|
||||
{"tax", pgtype.Int4ArrayOID},
|
||||
},
|
||||
conn.ConnInfo(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = registerType(ctx, conn, editedInvoiceProductType, editedInvoiceProductType.TypeName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cursor struct {
|
||||
PerPage int
|
||||
Pagination bool
|
||||
Name string
|
||||
Val string
|
||||
Colspan int
|
||||
}
|
||||
|
||||
func (cursor *Cursor) FillValue(r *http.Request) {
|
||||
cursor.Val = strings.TrimSpace(r.FormValue(cursor.Name))
|
||||
cursor.Pagination = cursor.Val != ""
|
||||
}
|
||||
|
||||
func (cursor *Cursor) Params() []string {
|
||||
return strings.Split(cursor.Val, ";")
|
||||
}
|
||||
|
||||
func BuildCursor[K interface{}](cursor *Cursor, elems []K, build func(K) []string) []K {
|
||||
if len(elems) <= cursor.PerPage {
|
||||
cursor.Val = ""
|
||||
return elems
|
||||
}
|
||||
elems = elems[:cursor.PerPage]
|
||||
cursor.Val = strings.Join(build(elems[cursor.PerPage-1]), ";")
|
||||
return elems
|
||||
}
|
|
@ -28,8 +28,17 @@ func (input *Input) setError(err error) {
|
|||
}
|
||||
|
||||
func (input *Input) FillValue(r *http.Request) {
|
||||
input.Val = strings.TrimSpace(r.FormValue(input.Name))
|
||||
input.FillValueIndex(r, 0)
|
||||
}
|
||||
|
||||
func (input *Input) FillValueIndex(r *http.Request, idx int) {
|
||||
var val string
|
||||
if vs := r.Form[input.Name]; len(vs) > idx {
|
||||
val = vs[idx]
|
||||
}
|
||||
input.Val = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
func (input *Input) Value() (driver.Value, error) {
|
||||
return input.Val, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Month struct {
|
||||
Name string
|
||||
Year int
|
||||
Month time.Month
|
||||
Error error
|
||||
}
|
||||
|
||||
func (input *Month) FillValue(r *http.Request) {
|
||||
if year, err := strconv.Atoi(r.FormValue(input.Name + ".year")); err == nil {
|
||||
input.Year = year
|
||||
} else {
|
||||
fmt.Println(err, r.FormValue(input.Name+".year"))
|
||||
}
|
||||
if month, err := strconv.Atoi(r.FormValue(input.Name + ".month")); err == nil && month > 0 && month < 13 {
|
||||
input.Month = time.Month(month)
|
||||
} else {
|
||||
fmt.Println(err, r.FormValue(input.Name+".month"))
|
||||
}
|
||||
}
|
||||
|
||||
func (input *Month) Date() time.Time {
|
||||
return time.Date(input.Year, input.Month, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
|
@ -8,6 +8,7 @@ package form
|
|||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
@ -48,7 +49,18 @@ func (s *Select) FillValue(r *http.Request) {
|
|||
s.Selected = r.Form[s.Name]
|
||||
}
|
||||
|
||||
func (s *Select) FillValueIndex(r *http.Request, idx int) {
|
||||
if vs := r.Form[s.Name]; len(vs) > idx {
|
||||
s.Selected = []string{vs[idx]}
|
||||
} else {
|
||||
s.Selected = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Select) ValidOptionsSelected() bool {
|
||||
if len(s.Selected) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, selected := range s.Selected {
|
||||
if !s.isValidOption(selected) {
|
||||
return false
|
||||
|
@ -102,3 +114,11 @@ func MustGetOptions(ctx context.Context, conn *database.Conn, sql string, args .
|
|||
|
||||
return options
|
||||
}
|
||||
|
||||
func MustGetCountryOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*Option {
|
||||
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language)
|
||||
}
|
||||
|
||||
func MustGetDocumentTypeOptions(ctx context.Context, conn *database.Conn, l *locale.Locale) []*Option {
|
||||
return MustGetOptions(ctx, conn, "select idt.id_document_type_id::text, coalesce(i18n.name, idt.name) as l10n_name from id_document_type as idt left join id_document_type_i18n as i18n on idt.id_document_type_id = i18n.id_document_type_id and i18n.lang_tag = $1 order by l10n_name", l.Language)
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
HxLocation = "HX-Location"
|
||||
HxRedirect = "HX-Redirect"
|
||||
HxRequest = "HX-Request"
|
||||
HxLocation = "HX-Location"
|
||||
HxRedirect = "HX-Redirect"
|
||||
HxRequest = "HX-Request"
|
||||
HxTriggerAfterSettle = "HX-Trigger-After-Settle"
|
||||
)
|
||||
|
||||
func Relocate(w http.ResponseWriter, r *http.Request, url string, code int) {
|
||||
|
@ -37,6 +38,10 @@ func Redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
|
|||
}
|
||||
}
|
||||
|
||||
func TriggerAfterSettle(w http.ResponseWriter, trigger string) {
|
||||
w.Header().Set(HxTriggerAfterSettle, trigger)
|
||||
}
|
||||
|
||||
func IsHTMxRequest(r *http.Request) bool {
|
||||
return r.Header.Get(HxRequest) == "true"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
|||
package invoice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
)
|
||||
|
||||
type filterForm struct {
|
||||
company *auth.Company
|
||||
Customer *form.Select
|
||||
InvoiceStatus *form.Select
|
||||
InvoiceNumber *form.Input
|
||||
FromDate *form.Input
|
||||
ToDate *form.Input
|
||||
Cursor *form.Cursor
|
||||
}
|
||||
|
||||
func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm {
|
||||
return &filterForm{
|
||||
company: company,
|
||||
Customer: &form.Select{
|
||||
Name: "customer",
|
||||
Options: mustGetContactOptions(ctx, conn, company),
|
||||
},
|
||||
InvoiceStatus: &form.Select{
|
||||
Name: "invoice_status",
|
||||
Options: mustGetInvoiceStatusOptions(ctx, conn, locale),
|
||||
},
|
||||
InvoiceNumber: &form.Input{
|
||||
Name: "number",
|
||||
},
|
||||
FromDate: &form.Input{
|
||||
Name: "from_date",
|
||||
},
|
||||
ToDate: &form.Input{
|
||||
Name: "to_date",
|
||||
},
|
||||
Cursor: &form.Cursor{
|
||||
Name: "cursor",
|
||||
PerPage: 25,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *filterForm) Parse(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Customer.FillValue(r)
|
||||
f.InvoiceStatus.FillValue(r)
|
||||
f.InvoiceNumber.FillValue(r)
|
||||
f.FromDate.FillValue(r)
|
||||
f.ToDate.FillValue(r)
|
||||
f.Cursor.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||
var where []string
|
||||
appendWhere := func(expression string, value interface{}) {
|
||||
args = append(args, value)
|
||||
where = append(where, fmt.Sprintf(expression, len(args)))
|
||||
}
|
||||
maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
|
||||
if value != "" {
|
||||
if conv == nil {
|
||||
appendWhere(expression, value)
|
||||
} else {
|
||||
appendWhere(expression, conv(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendWhere("invoice.company_id = $%d", f.company.ID)
|
||||
maybeAppendWhere("contact_id = $%d", f.Customer.String(), func(v string) interface{} {
|
||||
customerId, _ := strconv.Atoi(f.Customer.Selected[0])
|
||||
return customerId
|
||||
})
|
||||
maybeAppendWhere("invoice.invoice_status = $%d", f.InvoiceStatus.String(), nil)
|
||||
maybeAppendWhere("invoice_number = $%d", f.InvoiceNumber.Val, nil)
|
||||
maybeAppendWhere("invoice_date >= $%d", f.FromDate.Val, nil)
|
||||
maybeAppendWhere("invoice_date <= $%d", f.ToDate.Val, nil)
|
||||
|
||||
if f.Paginated() {
|
||||
params := f.Cursor.Params()
|
||||
if len(params) == 2 {
|
||||
where = append(where, fmt.Sprintf("(invoice_date, invoice_number) < ($%d, $%d)", len(args)+1, len(args)+2))
|
||||
args = append(args, params[0])
|
||||
args = append(args, params[1])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(where, ") AND ("), args
|
||||
}
|
||||
|
||||
func (f *filterForm) buildCursor(customers []*IndexEntry) []*IndexEntry {
|
||||
return form.BuildCursor(f.Cursor, customers, func(entry *IndexEntry) []string {
|
||||
return []string{entry.Date.Format(database.ISODateFormat), entry.Number}
|
||||
})
|
||||
}
|
||||
|
||||
func (f *filterForm) HasValue() bool {
|
||||
return (len(f.Customer.Selected) > 0 && f.Customer.Selected[0] != "") ||
|
||||
(len(f.InvoiceStatus.Selected) > 0 && f.InvoiceStatus.Selected[0] != "") ||
|
||||
f.InvoiceNumber.Val != "" ||
|
||||
f.FromDate.Val != "" ||
|
||||
f.ToDate.Val != ""
|
||||
}
|
||||
|
||||
func (f *filterForm) PerPage() int {
|
||||
return f.Cursor.PerPage
|
||||
}
|
||||
|
||||
func (f *filterForm) Paginated() bool {
|
||||
return f.Cursor.Pagination
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package invoice
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||
"dev.tandem.ws/tandem/camper/pkg/ods"
|
||||
)
|
||||
|
||||
func mustWriteInvoicesOds(invoices []*IndexEntry, taxes map[int]taxMap, taxColumns map[int]string, company *auth.Company, locale *locale.Locale) []byte {
|
||||
taxIDs := extractTaxIDs(taxColumns)
|
||||
columns := make([]string, 6+len(taxIDs))
|
||||
columns[0] = "Date"
|
||||
columns[1] = "Invoice Num."
|
||||
columns[2] = "Customer"
|
||||
columns[3] = "Status"
|
||||
i := 4
|
||||
for _, taxID := range taxIDs {
|
||||
columns[i] = taxColumns[taxID]
|
||||
i++
|
||||
}
|
||||
columns[i] = "Amount"
|
||||
table, err := ods.WriteTable(invoices, columns, locale, func(sb *strings.Builder, invoice *IndexEntry) error {
|
||||
ods.WriteCellDate(sb, invoice.Date)
|
||||
if err := ods.WriteCellString(sb, invoice.Number); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ods.WriteCellString(sb, invoice.CustomerName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ods.WriteCellString(sb, invoice.StatusLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
writeTaxes(sb, taxes[invoice.ID], taxIDs, company, locale)
|
||||
ods.WriteCellFloat(sb, invoice.Total, company, locale)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func extractTaxIDs(taxColumns map[int]string) []int {
|
||||
taxIDs := make([]int, len(taxColumns))
|
||||
i := 0
|
||||
for k := range taxColumns {
|
||||
taxIDs[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Ints(taxIDs[:])
|
||||
return taxIDs
|
||||
}
|
||||
|
||||
func writeTaxes(sb *strings.Builder, taxes taxMap, taxIDs []int, company *auth.Company, locale *locale.Locale) {
|
||||
for _, taxID := range taxIDs {
|
||||
var amount string
|
||||
if taxes != nil {
|
||||
amount = taxes[taxID]
|
||||
}
|
||||
ods.WriteCellFloat(sb, amount, company, locale)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package invoice
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
func mustWriteInvoicesPdf(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, slugs []string) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
w := zip.NewWriter(buf)
|
||||
for _, slug := range slugs {
|
||||
inv := mustGetInvoice(r.Context(), conn, company, slug)
|
||||
if inv == nil {
|
||||
continue
|
||||
}
|
||||
f, err := w.Create(fmt.Sprintf("%s-%s.pdf", inv.Number, template.Slugify(inv.Invoicee.Name)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mustWriteInvoicePdf(f, r, user, company, inv)
|
||||
}
|
||||
mustClose(w)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func mustWriteInvoicePdf(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, inv *invoice) {
|
||||
cmd := exec.Command("weasyprint", "--stylesheet", "web/static/invoice.css", "-", "-")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
err := stdout.Close()
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
if err = cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go func() {
|
||||
defer mustClose(stdin)
|
||||
template.MustRenderAdmin(stdin, r, user, company, "invoice/view.gohtml", inv)
|
||||
}()
|
||||
if _, err = io.Copy(w, stdout); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Printf("ERR - %v\n", stderr.String())
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustClose(closer io.Closer) {
|
||||
if err := closer.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue