Add the campsite type cost per season

This commit is contained in:
jordi fita mas 2023-10-01 21:14:39 +02:00
parent 680d51e704
commit ef6a8f5aee
27 changed files with 1059 additions and 38 deletions

View File

@ -265,5 +265,20 @@ select set_season_range(94, '[2023-09-24, 2023-09-28]');
select set_season_range(93, '[2023-09-29, 2023-09-30]');
select set_season_range(94, '[2023-10-01, 2023-10-12]');
insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights)
values (72, 92, 20000, 2)
, (72, 93, 16500, 2)
, (72, 94, 12500, 2)
, (73, 92, 20000, 2)
, (73, 93, 16500, 2)
, (73, 94, 12500, 2)
, (74, 92, 20000, 2)
, (74, 93, 16500, 2)
, (74, 94, 12500, 2)
, (75, 92, 20000, 2)
, (75, 93, 16500, 2)
, (75, 94, 12500, 2)
;
commit;

View File

@ -0,0 +1,56 @@
-- Deploy camper:campsite_type_cost to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type
-- requires: season
-- requires: user_profile
begin;
set search_path to camper, public;
create table campsite_type_cost (
campsite_type_id integer not null references campsite_type,
season_id integer not null references season,
cost_per_night integer not null constraint cost_not_negative check(cost_per_night >= 0),
min_nights integer not null constraint at_least_one_night check(min_nights > 0),
primary key (campsite_type_id, season_id)
);
grant select on table campsite_type_cost to guest;
grant select on table campsite_type_cost to employee;
grant select, insert, update, delete on table campsite_type_cost to admin;
alter table campsite_type_cost enable row level security;
create policy guest_ok
on campsite_type_cost
for select
using (true)
;
create policy insert_to_company
on campsite_type_cost
for insert
with check (
exists (select 1 from campsite_type join season using (company_id) join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_cost.campsite_type_id and season.season_id = campsite_type_cost.season_id)
)
;
create policy update_company
on campsite_type_cost
for update
using (
exists (select 1 from campsite_type join season using (company_id) join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_cost.campsite_type_id and season.season_id = campsite_type_cost.season_id)
)
;
create policy delete_from_company
on campsite_type_cost
for delete
using (
exists (select 1 from campsite_type join season using (company_id) join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_cost.campsite_type_id and season.season_id = campsite_type_cost.season_id)
)
;
commit;

57
deploy/parse_price.sql Normal file
View File

@ -0,0 +1,57 @@
-- Deploy camper:parse_price to pg
-- requires: roles
-- requires: schema_camper
begin;
set search_path to camper, public;
create or replace function parse_price(price text, decimal_digits integer default 2) returns integer as
$$
declare
parts text[];
result int;
frac_part text;
sign int := 1;
begin
if price like '-%' Then
sign := -1;
price := substring(price from 2);
end if;
parts := string_to_array(price, '.');
if array_length(parts, 1) > 2 then
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
end if;
result := parts[1]::integer;
if result is null then
raise invalid_parameter_value using message = price || ' is not a valid price representation.';
end if;
for d in 1..decimal_digits loop
result := result * 10;
end loop;
if array_length(parts, 1) = 2 then
frac_part := rtrim(parts[2], '0');
if length(frac_part) > decimal_digits then
raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.';
end if;
frac_part := rpad(frac_part, decimal_digits, '0');
result := result + frac_part::integer;
end if;
return sign * result;
end;
$$
language plpgsql
immutable;
comment on function parse_price(text, integer) is
'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.';
revoke execute on function parse_price(text, integer) from public;
grant execute on function parse_price(text, integer) to employee;
grant execute on function parse_price(text, integer) to admin;
commit;

View File

@ -0,0 +1,28 @@
-- Deploy camper:set_campsite_type_cost to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_cost
-- requires: parse_price
begin;
set search_path to camper, public;
create or replace function set_campsite_type_cost(slug uuid, season_id integer, min_nights integer, cost_per_night text, decimal_places integer default 2) returns void as
$$
insert into campsite_type_cost (campsite_type_id, season_id, min_nights, cost_per_night)
select campsite_type_id, season_id, min_nights, parse_price(cost_per_night, decimal_places)
from campsite_type
where campsite_type.slug = set_campsite_type_cost.slug
on conflict (campsite_type_id, season_id) do update
set min_nights = excluded.min_nights
, cost_per_night = excluded.cost_per_night
;
$$
language sql
;
revoke execute on function set_campsite_type_cost(uuid, integer, integer, text, integer) from public;
grant execute on function set_campsite_type_cost(uuid, integer, integer, text, integer) to admin;
commit;

34
deploy/to_price.sql Normal file
View File

@ -0,0 +1,34 @@
-- Deploy camper:to_price to pg
-- requires: roles
-- requires: schema_camper
begin;
set search_path to camper, public;
create or replace function to_price(cents integer, decimal_digits integer default 2) returns text as
$$
declare
result text;
scale integer := 10^decimal_digits;
sign text := '';
begin
if cents < 0 then
sign := '-';
cents = -cents;
end if;
result = cents::text;
return sign || (cents / scale)::text || '.' || to_char(mod(cents, scale), rpad('FM', decimal_digits + 2, '0'));
end;
$$
language plpgsql
immutable;
comment on function to_price(integer, integer) is
'Converts the cents to a price representation, without currency and any other separater than decimal.';
revoke execute on function to_price(integer, integer) from public;
grant execute on function to_price(integer, integer) to employee;
grant execute on function to_price(integer, integer) to admin;
commit;

View File

@ -7,6 +7,7 @@ package types
import (
"context"
"fmt"
"net/http"
"github.com/jackc/pgx/v4"
@ -37,7 +38,10 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
case "new":
switch r.Method {
case http.MethodGet:
f := newTypeForm()
f, err := newTypeForm(r.Context(), company, conn)
if err != nil {
panic(err)
}
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
@ -56,7 +60,10 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
http.NotFound(w, r)
return
}
f := newTypeForm()
f, err := newTypeForm(r.Context(), company, conn)
if err != nil {
panic(err)
}
if err := f.FillFromDatabase(r.Context(), conn, head); err != nil {
if database.ErrorIsNotFound(err) {
http.NotFound(w, r)
@ -181,19 +188,38 @@ func (page *typeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *
}
func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newTypeForm()
processTypeForm(w, r, user, company, conn, f, func(ctx context.Context) {
conn.MustExec(ctx, "select add_campsite_type($1, $2, $3, $4, $5, $6)", company.ID, f.Media, f.Name, f.Description, f.MaxCampers, f.DogsAllowed)
f, err := newTypeForm(r.Context(), company, conn)
if err != nil {
panic(err)
}
processTypeForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
slug, err := tx.GetText(ctx, "select add_campsite_type($1, $2, $3, $4, $5, $6)", company.ID, f.Media, f.Name, f.Description, f.MaxCampers, f.DogsAllowed)
if err != nil {
return err
}
return setPrices(ctx, tx, slug, f.Prices)
})
}
func setPrices(ctx context.Context, tx *database.Tx, slug string, prices map[int]*typePriceForm) error {
for seasonID, p := range prices {
if _, err := tx.Exec(ctx, "select set_campsite_type_cost($1, $2, $3, $4)", slug, seasonID, p.MinNights, p.PricePerNight); err != nil {
return err
}
}
return nil
}
func editType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) {
processTypeForm(w, r, user, company, conn, f, func(ctx context.Context) {
conn.MustExec(ctx, "select edit_campsite_type($1, $2, $3, $4, $5, $6, $7)", f.Slug, f.Media, f.Name, f.Description, f.MaxCampers, f.DogsAllowed, f.Active)
processTypeForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
if _, err := conn.Exec(ctx, "select edit_campsite_type($1, $2, $3, $4, $5, $6, $7)", f.Slug, f.Media, f.Name, f.Description, f.MaxCampers, f.DogsAllowed, f.Active); err != nil {
return err
}
return setPrices(ctx, tx, f.Slug, f.Prices)
})
}
func processTypeForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm, act func(ctx context.Context)) {
func processTypeForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm, act func(ctx context.Context, tx *database.Tx) error) {
if err := f.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -211,7 +237,14 @@ func processTypeForm(w http.ResponseWriter, r *http.Request, user *auth.User, co
f.MustRender(w, r, user, company)
return
}
act(r.Context())
tx := conn.MustBegin(r.Context())
if err := act(r.Context(), tx); err == nil {
tx.Commit(r.Context())
} else {
tx.Rollback(r.Context())
panic(err)
}
httplib.Redirect(w, r, "/admin/campsites/types", http.StatusSeeOther)
}
@ -223,10 +256,17 @@ type typeForm struct {
MaxCampers *form.Input
DogsAllowed *form.Checkbox
Description *form.Input
Prices map[int]*typePriceForm
}
func newTypeForm() *typeForm {
return &typeForm{
type typePriceForm struct {
SeasonName string
PricePerNight *form.Input
MinNights *form.Input
}
func newTypeForm(ctx context.Context, company *auth.Company, conn *database.Conn) (*typeForm, error) {
f := &typeForm{
Active: &form.Checkbox{
Name: "active",
Checked: true,
@ -251,6 +291,34 @@ func newTypeForm() *typeForm {
Name: "description",
},
}
rows, err := conn.Query(ctx, "select season_id, name from season where active and company_id = $1", company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
f.Prices = make(map[int]*typePriceForm)
for rows.Next() {
var id int
var name string
if err = rows.Scan(&id, &name); err != nil {
return nil, err
}
f.Prices[id] = &typePriceForm{
SeasonName: name,
PricePerNight: &form.Input{
Name: fmt.Sprintf("season.%d.price_per_night", id),
Val: "0",
},
MinNights: &form.Input{
Name: fmt.Sprintf("season.%d.min_nights", id),
Val: "1",
},
}
}
return f, nil
}
func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
@ -265,7 +333,36 @@ func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, sl
from campsite_type
where slug = $1
`, slug)
return row.Scan(&f.Name.Val, &f.Description.Val, &f.Media.Val, &f.MaxCampers.Val, &f.DogsAllowed.Checked, &f.Active.Checked)
if err := row.Scan(&f.Name.Val, &f.Description.Val, &f.Media.Val, &f.MaxCampers.Val, &f.DogsAllowed.Checked, &f.Active.Checked); err != nil {
return err
}
rows, err := conn.Query(ctx, `
select season_id
, min_nights::text
, to_price(cost_per_night)
from campsite_type
join campsite_type_cost using (campsite_type_id)
where slug = $1
`, slug)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var seasonID int
var minNights string
var pricePerNight string
if err := rows.Scan(&seasonID, &minNights, &pricePerNight); err != nil {
return err
}
if p, ok := f.Prices[seasonID]; ok {
p.MinNights.Val = minNights
p.PricePerNight.Val = pricePerNight
}
}
return rows.Err()
}
func (f *typeForm) Parse(r *http.Request) error {
@ -278,6 +375,10 @@ func (f *typeForm) Parse(r *http.Request) error {
f.DogsAllowed.FillValue(r)
f.Description.FillValue(r)
f.Media.FillValue(r)
for _, p := range f.Prices {
p.PricePerNight.FillValue(r)
p.MinNights.FillValue(r)
}
return nil
}
@ -296,6 +397,18 @@ func (f *typeForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Loc
v.CheckMinInteger(f.MaxCampers, 1, l.GettextNoop("Maximum number of campers must be one or greater."))
}
}
for _, p := range f.Prices {
if v.CheckRequired(p.PricePerNight, l.GettextNoop("Price per night can not be empty.")) {
if v.CheckValidDecimal(p.PricePerNight, l.GettextNoop("Price per night must be a decimal number.")) {
v.CheckMinDecimal(p.PricePerNight, 0.0, l.GettextNoop("Price per night must be zero or greater."))
}
}
if v.CheckRequired(p.MinNights, l.GettextNoop("Minimum number of nights can not be empty.")) {
if v.CheckValidInteger(p.MinNights, l.GettextNoop("Minimum number of nights must be an integer.")) {
v.CheckMinInteger(p.MinNights, 0, l.GettextNoop("Minimum number of nights must be one or greater."))
}
}
}
return v.AllOK, nil
}

View File

@ -48,9 +48,17 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
type publicPage struct {
*template.PublicPage
Name string
Prices []*typePrice
Description gotemplate.HTML
}
type typePrice struct {
SeasonName string
SeasonColor string
MinNights int
PricePerNight string
}
func newPublicPage(ctx context.Context, user *auth.User, conn *database.Conn, slug string) (*publicPage, error) {
page := &publicPage{
PublicPage: template.NewPublicPage(),
@ -67,6 +75,30 @@ func newPublicPage(ctx context.Context, user *auth.User, conn *database.Conn, sl
return nil, err
}
rows, err := conn.Query(ctx, `
select season.name --coalesce(i18n.name, season.name) as l10n_name
, to_color(season.color)
, coalesce(min_nights, 1)
, to_price(coalesce(cost_per_night, 0))::text
from season
--left join season_i18n as i18n on season.season_id = i18n.season_id and i18n.lang_tag = $1
left join (
campsite_type_cost as cost join campsite_type as type on cost.campsite_type_id = type.campsite_type_id and type.slug = $1
) as cost on cost.season_id = season.season_id
where season.active
`, slug)
if err != nil {
return nil, err
}
for rows.Next() {
price := &typePrice{}
if err := rows.Scan(&price.SeasonName, &price.SeasonColor, &price.MinNights, &price.PricePerNight); err != nil {
return nil, err
}
page.Prices = append(page.Prices, price)
}
return page, nil
}

View File

@ -29,6 +29,12 @@ func (tx *Tx) MustExec(ctx context.Context, sql string, args ...interface{}) pgc
return tag
}
func (tx *Tx) GetText(ctx context.Context, sql string, args ...interface{}) (string, error) {
var result string
err := tx.QueryRow(ctx, sql, args...).Scan(&result)
return result, err
}
func (tx *Tx) GetInt(ctx context.Context, sql string, args ...interface{}) (int, error) {
var result int
err := tx.QueryRow(ctx, sql, args...).Scan(&result)

View File

@ -43,11 +43,21 @@ func (v *Validator) CheckMinInteger(input *Input, min int, message string) bool
return v.Check(input, i >= min, message)
}
func (v *Validator) CheckMinDecimal(input *Input, min float64, message string) bool {
f, _ := strconv.ParseFloat(input.Val, 64)
return v.Check(input, f >= min, message)
}
func (v *Validator) CheckValidInteger(input *Input, message string) bool {
_, err := strconv.Atoi(input.Val)
return v.Check(input, err == nil, message)
}
func (v *Validator) CheckValidDecimal(input *Input, message string) bool {
_, err := strconv.ParseFloat(input.Val, 64)
return v.Check(input, err == nil, message)
}
func (v *Validator) CheckValidEmail(input *Input, message string) bool {
_, err := mail.ParseAddress(input.Val)
return v.Check(input, err == nil, message)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-09-29 20:08+0200\n"
"POT-Creation-Date: 2023-10-01 21:09+0200\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -74,6 +74,20 @@ msgstr "Descobreix lentorn"
msgid "Come and enjoy!"
msgstr "Vine a gaudir!"
#: web/templates/public/campsite/type.gohtml:17
#: web/templates/admin/campsite/type/form.gohtml:73
msgctxt "title"
msgid "Prices"
msgstr "Preus"
#: web/templates/public/campsite/type.gohtml:26
msgid "%s €/night"
msgstr "%s €/nit"
#: web/templates/public/campsite/type.gohtml:28
msgid "*Minimum %d nights per stay"
msgstr "*Mínim %d nits per estada"
#: web/templates/public/surroundings.gohtml:13
msgctxt "title"
msgid "What to Do Outside the Campsite?"
@ -165,7 +179,7 @@ msgstr "Llegenda"
#: web/templates/admin/carousel/form.gohtml:47
#: web/templates/admin/campsite/form.gohtml:70
#: web/templates/admin/campsite/type/form.gohtml:82
#: web/templates/admin/campsite/type/form.gohtml:108
#: web/templates/admin/season/form.gohtml:64
#: web/templates/admin/services/form.gohtml:69
#: web/templates/admin/media/form.gohtml:35
@ -175,7 +189,7 @@ msgstr "Actualitza"
#: web/templates/admin/carousel/form.gohtml:49
#: web/templates/admin/campsite/form.gohtml:72
#: web/templates/admin/campsite/type/form.gohtml:84
#: web/templates/admin/campsite/type/form.gohtml:110
#: web/templates/admin/season/form.gohtml:66
#: web/templates/admin/services/form.gohtml:71
msgctxt "action"
@ -320,7 +334,17 @@ msgctxt "input"
msgid "Dogs allowed"
msgstr "Es permeten gossos"
#: web/templates/admin/campsite/type/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:79
msgctxt "input"
msgid "Price per night"
msgstr "Preu per nit"
#: web/templates/admin/campsite/type/form.gohtml:87
msgctxt "input"
msgid "Minimum number of nights"
msgstr "Número mínim de nits"
#: web/templates/admin/campsite/type/form.gohtml:99
#: web/templates/admin/campsite/type/l10n.gohtml:32
#: web/templates/admin/services/form.gohtml:60
#: web/templates/admin/services/l10n.gohtml:32
@ -812,7 +836,7 @@ msgid "Automatic"
msgstr "Automàtic"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82
#: pkg/campsite/types/admin.go:286 pkg/season/admin.go:335
#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335
#: pkg/services/l10n.go:73 pkg/services/admin.go:266
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
@ -833,40 +857,64 @@ msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
msgid "Access forbidden"
msgstr "Accés prohibit"
#: pkg/campsite/types/admin.go:238
#: pkg/campsite/types/admin.go:278
msgctxt "input"
msgid "Cover image"
msgstr "Imatge de portada"
#: pkg/campsite/types/admin.go:239
#: pkg/campsite/types/admin.go:279
msgctxt "action"
msgid "Set campsite type cover"
msgstr "Estableix la portada del tipus dallotjament"
#: pkg/campsite/types/admin.go:287
#: pkg/campsite/types/admin.go:388
msgid "Name must have at least one letter."
msgstr "El nom ha de tenir com a mínim una lletra."
#: pkg/campsite/types/admin.go:289
#: pkg/campsite/types/admin.go:390
msgid "Cover image can not be empty."
msgstr "No podeu deixar la imatge de portada en blanc."
#: pkg/campsite/types/admin.go:290
#: pkg/campsite/types/admin.go:391
msgid "Cover image must be an image media type."
msgstr "La imatge de portada ha de ser un mèdia de tipus imatge."
#: pkg/campsite/types/admin.go:294
#: pkg/campsite/types/admin.go:395
msgid "Maximum number of campers can not be empty."
msgstr "No podeu deixar el número màxim de persones en blanc."
#: pkg/campsite/types/admin.go:295
#: pkg/campsite/types/admin.go:396
msgid "Maximum number of campers must be an integer number."
msgstr "El número màxim de persones ha de ser enter."
#: pkg/campsite/types/admin.go:296
#: pkg/campsite/types/admin.go:397
msgid "Maximum number of campers must be one or greater."
msgstr "El número màxim de persones no pot ser zero."
#: pkg/campsite/types/admin.go:401
msgid "Price per night can not be empty."
msgstr "No podeu deixar el preu per nit en blanc."
#: pkg/campsite/types/admin.go:402
msgid "Price per night must be a decimal number."
msgstr "El preu per nit ha de ser un número decimal."
#: pkg/campsite/types/admin.go:403
msgid "Price per night must be zero or greater."
msgstr "El preu per nit ha de ser com a mínim zero."
#: pkg/campsite/types/admin.go:406
msgid "Minimum number of nights can not be empty."
msgstr "No podeu deixar el número mínim de nits en blanc."
#: pkg/campsite/types/admin.go:407
msgid "Minimum number of nights must be an integer."
msgstr "El número mínim de nits ha de ser enter."
#: pkg/campsite/types/admin.go:408
msgid "Minimum number of nights must be one or greater."
msgstr "El número mínim de nits no pot ser zero."
#: pkg/campsite/admin.go:226
msgid "Selected campsite type is not valid."
msgstr "El tipus dallotjament escollit no és vàlid."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-09-29 20:08+0200\n"
"POT-Creation-Date: 2023-10-01 21:09+0200\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -74,6 +74,20 @@ msgstr "Descubre el entorno"
msgid "Come and enjoy!"
msgstr "¡Ven a disfrutar!"
#: web/templates/public/campsite/type.gohtml:17
#: web/templates/admin/campsite/type/form.gohtml:73
msgctxt "title"
msgid "Prices"
msgstr "Precios"
#: web/templates/public/campsite/type.gohtml:26
msgid "%s €/night"
msgstr "%s €/noche"
#: web/templates/public/campsite/type.gohtml:28
msgid "*Minimum %d nights per stay"
msgstr "*Mínimo %d noches por estancia"
#: web/templates/public/surroundings.gohtml:13
msgctxt "title"
msgid "What to Do Outside the Campsite?"
@ -165,7 +179,7 @@ msgstr "Leyenda"
#: web/templates/admin/carousel/form.gohtml:47
#: web/templates/admin/campsite/form.gohtml:70
#: web/templates/admin/campsite/type/form.gohtml:82
#: web/templates/admin/campsite/type/form.gohtml:108
#: web/templates/admin/season/form.gohtml:64
#: web/templates/admin/services/form.gohtml:69
#: web/templates/admin/media/form.gohtml:35
@ -175,7 +189,7 @@ msgstr "Actualizar"
#: web/templates/admin/carousel/form.gohtml:49
#: web/templates/admin/campsite/form.gohtml:72
#: web/templates/admin/campsite/type/form.gohtml:84
#: web/templates/admin/campsite/type/form.gohtml:110
#: web/templates/admin/season/form.gohtml:66
#: web/templates/admin/services/form.gohtml:71
msgctxt "action"
@ -320,7 +334,17 @@ msgctxt "input"
msgid "Dogs allowed"
msgstr "Se permiten perros"
#: web/templates/admin/campsite/type/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:79
msgctxt "input"
msgid "Price per night"
msgstr "Precio por noche"
#: web/templates/admin/campsite/type/form.gohtml:87
msgctxt "input"
msgid "Minimum number of nights"
msgstr "Número mínimos de noches"
#: web/templates/admin/campsite/type/form.gohtml:99
#: web/templates/admin/campsite/type/l10n.gohtml:32
#: web/templates/admin/services/form.gohtml:60
#: web/templates/admin/services/l10n.gohtml:32
@ -812,7 +836,7 @@ msgid "Automatic"
msgstr "Automático"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82
#: pkg/campsite/types/admin.go:286 pkg/season/admin.go:335
#: pkg/campsite/types/admin.go:387 pkg/season/admin.go:335
#: pkg/services/l10n.go:73 pkg/services/admin.go:266
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
@ -833,40 +857,64 @@ msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
msgid "Access forbidden"
msgstr "Acceso prohibido"
#: pkg/campsite/types/admin.go:238
#: pkg/campsite/types/admin.go:278
msgctxt "input"
msgid "Cover image"
msgstr "Imagen de portada"
#: pkg/campsite/types/admin.go:239
#: pkg/campsite/types/admin.go:279
msgctxt "action"
msgid "Set campsite type cover"
msgstr "Establecer la portada del tipo de alojamiento"
#: pkg/campsite/types/admin.go:287
#: pkg/campsite/types/admin.go:388
msgid "Name must have at least one letter."
msgstr "El nombre tiene que tener como mínimo una letra."
#: pkg/campsite/types/admin.go:289
#: pkg/campsite/types/admin.go:390
msgid "Cover image can not be empty."
msgstr "No podéis dejar la imagen de portada en blanco."
#: pkg/campsite/types/admin.go:290
#: pkg/campsite/types/admin.go:391
msgid "Cover image must be an image media type."
msgstr "La imagen de portada tiene que ser un medio de tipo imagen."
#: pkg/campsite/types/admin.go:294
#: pkg/campsite/types/admin.go:395
msgid "Maximum number of campers can not be empty."
msgstr "No podéis dejar el número máximo de personas en blanco."
#: pkg/campsite/types/admin.go:295
#: pkg/campsite/types/admin.go:396
msgid "Maximum number of campers must be an integer number."
msgstr "El número máximo de personas tiene que ser entero."
#: pkg/campsite/types/admin.go:296
#: pkg/campsite/types/admin.go:397
msgid "Maximum number of campers must be one or greater."
msgstr "El número máximo de personas no puede ser cero."
#: pkg/campsite/types/admin.go:401
msgid "Price per night can not be empty."
msgstr "No podéis dejar el precio por noche en blanco."
#: pkg/campsite/types/admin.go:402
msgid "Price per night must be a decimal number."
msgstr "El precio por noche tien que ser un número decimal."
#: pkg/campsite/types/admin.go:403
msgid "Price per night must be zero or greater."
msgstr "El precio por noche tiene que ser como mínimo cero."
#: pkg/campsite/types/admin.go:406
msgid "Minimum number of nights can not be empty."
msgstr "No podéis dejar el número mínimo de noches en blanco."
#: pkg/campsite/types/admin.go:407
msgid "Minimum number of nights must be an integer."
msgstr "El número mínimo de noches tiene que ser entero."
#: pkg/campsite/types/admin.go:408
msgid "Minimum number of nights must be one or greater."
msgstr "El número mínimo de noches no puede ser cero."
#: pkg/campsite/admin.go:226
msgid "Selected campsite type is not valid."
msgstr "El tipo de alojamiento escogido no es válido."

View File

@ -0,0 +1,7 @@
-- Revert camper:campsite_type_cost from pg
begin;
drop table if exists camper.campsite_type_cost;
commit;

7
revert/parse_price.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:parse_price from pg
begin;
drop function if exists camper.parse_price(text, integer);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:set_campsite_type_cost from pg
begin;
drop function if exists camper.set_campsite_type_cost(uuid, integer, integer, text, integer);
commit;

7
revert/to_price.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:to_price from pg
begin;
drop function if exists camper.to_price(integer, integer);
commit;

View File

@ -84,3 +84,7 @@ extension_btree_gist [schema_public] 2023-09-26T17:49:30Z jordi fita mas <jordi@
season_calendar [roles schema_camper season extension_btree_gist user_profile] 2023-09-26T18:07:21Z jordi fita mas <jordi@tandem.blog> # Add the relation of date ranges for seasons
unset_season_range [roles schema_camper season_calendar] 2023-09-26T21:56:38Z jordi fita mas <jordi@tandem.blog> # Add function to unset a date range from the seasons calendar
set_season_range [roles schema_camper season_calendar unset_season_range] 2023-09-26T18:37:29Z jordi fita mas <jordi@tandem.blog> # Add function to set a seasons date range
campsite_type_cost [roles schema_camper campsite_type season user_profile] 2023-10-01T15:45:37Z jordi fita mas <jordi@tandem.blog> # Add relation of costs for campsite types
parse_price [roles schema_camper] 2023-10-01T16:27:50Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas <jordi@tandem.blog> # Add function to format cents to prices
set_campsite_type_cost [roles schema_camper campsite_type_cost parse_price] 2023-10-01T17:51:23Z jordi fita mas <jordi@tandem.blog> # Add function to set the cost of a campsite type for a given season

246
test/campsite_type_cost.sql Normal file
View File

@ -0,0 +1,246 @@
-- Test campsite_type_cost
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(47);
set search_path to camper, public;
select has_table('campsite_type_cost');
select has_pk('campsite_type_cost');
select col_is_pk('campsite_type_cost', array['campsite_type_id', 'season_id']);
select table_privs_are('campsite_type_cost', 'guest', array['SELECT']);
select table_privs_are('campsite_type_cost', 'employee', array['SELECT']);
select table_privs_are('campsite_type_cost', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('campsite_type_cost', 'authenticator', array[]::text[]);
select has_column('campsite_type_cost', 'campsite_type_id');
select col_is_fk('campsite_type_cost', 'campsite_type_id');
select fk_ok('campsite_type_cost', 'campsite_type_id', 'campsite_type', 'campsite_type_id');
select col_type_is('campsite_type_cost', 'campsite_type_id', 'integer');
select col_not_null('campsite_type_cost', 'campsite_type_id');
select col_hasnt_default('campsite_type_cost', 'campsite_type_id');
select has_column('campsite_type_cost', 'season_id');
select col_is_fk('campsite_type_cost', 'season_id');
select fk_ok('campsite_type_cost', 'season_id', 'season', 'season_id');
select col_type_is('campsite_type_cost', 'season_id', 'integer');
select col_not_null('campsite_type_cost', 'season_id');
select col_hasnt_default('campsite_type_cost', 'season_id');
select has_column('campsite_type_cost', 'cost_per_night');
select col_type_is('campsite_type_cost', 'cost_per_night', 'integer');
select col_not_null('campsite_type_cost', 'cost_per_night');
select col_hasnt_default('campsite_type_cost', 'cost_per_night');
select has_column('campsite_type_cost', 'min_nights');
select col_type_is('campsite_type_cost', 'min_nights', 'integer');
select col_not_null('campsite_type_cost', 'min_nights');
select col_hasnt_default('campsite_type_cost', 'min_nights');
set client_min_messages to warning;
truncate campsite_type_cost cascade;
truncate season cascade;
truncate campsite_type cascade;
truncate media cascade;
truncate media_content cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into media_content (media_type, bytes)
values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
;
insert into media (media_id, company_id, original_filename, content_hash)
values (6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
, (8, 4, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
;
insert into campsite_type (campsite_type_id, company_id, name, media_id, dogs_allowed, max_campers)
values (16, 2, 'Wooden lodge', 6, false, 7)
, (18, 4, 'Bungalow', 8, false, 6)
;
insert into season (season_id, company_id, name)
values (26, 2, 'Low')
, (27, 2, 'Mid')
, (28, 4, 'Low')
, (29, 4, 'Mid')
;
insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights)
values (16, 26, 2, 2)
, (18, 28, 4, 4)
;
prepare campsite_season_data as
select campsite_type_id, season_id, cost_per_night
from campsite_type_cost
;
set role guest;
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (18, 28, 4)
$$,
'Everyone should be able to list all campsite type costs across all companies'
);
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select lives_ok(
$$ insert into campsite_type_cost(campsite_type_id, season_id, cost_per_night, min_nights) values (16, 27, 3, 3) $$,
'Admin from company 2 should be able to insert a new campsite type season to that company.'
);
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (16, 27, 3)
, (18, 28, 4)
$$,
'The new row should have been added'
);
select lives_ok(
$$ update campsite_type_cost set cost_per_night = 6 where campsite_type_id = 16 and season_id = 27 $$,
'Admin from company 2 should be able to update campsite type season of that company.'
);
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (16, 27, 6)
, (18, 28, 4)
$$,
'The row should have been updated.'
);
select lives_ok(
$$ delete from campsite_type_cost where campsite_type_id = 16 and season_id = 27 $$,
'Admin from company 2 should be able to delete campsite type season from that company.'
);
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (18, 28, 4)
$$,
'The row should have been deleted.'
);
select throws_ok(
$$ insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) values (18, 29, 5, 5) $$,
'42501', 'new row violates row-level security policy for table "campsite_type_cost"',
'Admin from company 2 should NOT be able to insert new campsite type costs to company 4.'
);
select throws_ok(
$$ insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) values (18, 27, 5, 5) $$,
'42501', 'new row violates row-level security policy for table "campsite_type_cost"',
'Admin from company 2 should NOT be able to insert new row with a campsite type from company 4.'
);
select throws_ok(
$$ insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) values (16, 29, 5, 5) $$,
'42501', 'new row violates row-level security policy for table "campsite_type_cost"',
'Admin from company 2 should NOT be able to insert new row with a season from company 4.'
);
select lives_ok(
$$ update campsite_type_cost set cost_per_night = 1 where campsite_type_id = 18 $$,
'Admin from company 2 should not be able to update campsite types of company 4, but no error if campsite_type_id is not changed.'
);
select lives_ok(
$$ update campsite_type_cost set cost_per_night = 1 where season_id = 28 $$,
'Admin from company 2 should not be able to update seasons of company 4, but no error if season_id is not changed.'
);
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (18, 28, 4)
$$,
'No row should have been changed.'
);
select throws_ok(
$$ update campsite_type_cost set campsite_type_id = 18 where campsite_type_id = 16 $$,
'42501', 'new row violates row-level security policy for table "campsite_type_cost"',
'Admin from company 2 should NOT be able to move campsite type to one of company 4'
);
select throws_ok(
$$ update campsite_type_cost set season_id = 29 where season_id = 26 $$,
'42501', 'new row violates row-level security policy for table "campsite_type_cost"',
'Admin from company 2 should NOT be able to move season to one of company 4'
);
select lives_ok(
$$ delete from campsite_type_cost where campsite_type_id = 18 $$,
'Admin from company 2 should NOT be able to delete campsite type from company 4, but not error is thrown'
);
select lives_ok(
$$ delete from campsite_type_cost where season_id = 28 $$,
'Admin from company 2 should NOT be able to delete season from company 4, but not error is thrown'
);
select bag_eq(
'campsite_season_data',
$$ values (16, 26, 2)
, (18, 28, 4)
$$,
'No row should have been changed'
);
select throws_ok(
$$ insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) values (16, 27, -1, 1) $$,
'23514', 'new row for relation "campsite_type_cost" violates check constraint "cost_not_negative"',
'Should not be able to insert campsite type costs with negative cost per night.'
);
select throws_ok(
$$ insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) values (16, 27, 1, 0) $$,
'23514', 'new row for relation "campsite_type_cost" violates check constraint "at_least_one_night"',
'Should not be able to insert campsite type costs with at a night stay.'
);
reset role;
select *
from finish();
rollback;

61
test/parse_price.sql Normal file
View File

@ -0,0 +1,61 @@
-- Test parse_price
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(44);
set search_path to auth, camper, public;
select has_function('camper', 'parse_price', array ['text', 'integer']);
select function_lang_is('camper', 'parse_price', array ['text', 'integer'], 'plpgsql');
select function_returns('camper', 'parse_price', array ['text', 'integer'], 'integer');
select isnt_definer('camper', 'parse_price', array ['text', 'integer']);
select volatility_is('camper', 'parse_price', array ['text', 'integer'], 'immutable');
select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'guest', array []::text[]);
select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'authenticator', array []::text[]);
select is( parse_price('1.1', 2), 110 );
select is( parse_price('1.1', 3), 1100 );
select is( parse_price('0', 2), 0 );
select is( parse_price('0', 3), 0 );
select is( parse_price('-0', 2), 0 );
select is( parse_price('-0', 3), 0 );
select is( parse_price('0.01', 2), 1 );
select is( parse_price('0.001', 3), 1 );
select is( parse_price('-0.01', 2), -1 );
select is( parse_price('-0.001', 3), -1 );
select is( parse_price('0.1', 2), 10 );
select is( parse_price('0.01', 3), 10 );
select is( parse_price('1', 2), 100 );
select is( parse_price('0.1', 3), 100 );
select is( parse_price('10', 2), 1000 );
select is( parse_price('1', 3), 1000 );
select is( parse_price('23.23', 2), 2323 );
select is( parse_price('23.23', 3), 23230 );
select is( parse_price('-23.23', 2), -2323 );
select is( parse_price('-23.23', 3), -23230 );
select throws_ok( $$ select parse_price('234.234', 2) $$ );
select is( parse_price('234.234', 3), 234234 );
select throws_ok( $$ select parse_price('2345.2345', 2) $$ );
select throws_ok( $$ select parse_price('2345.2345', 3) $$ );
select is( parse_price('00000000000000001.100000000000000000000', 2), 110 );
select is( parse_price('00000000000000001.100000000000000000000', 3), 1100 );
select is( parse_price('00000000000000000.100000000000000000000', 2), 10 );
select is( parse_price('00000000000000000.100000000000000000000', 3), 100 );
select is( parse_price('00000000000123456.780000000000000000000', 2), 12345678 );
select is( parse_price('00000000000123456.789000000000000000000', 3), 123456789 );
select throws_ok( $$ select parse_price('1,1', 2) $$ );
select throws_ok( $$ select parse_price('1.1.1', 2) $$ );
select throws_ok( $$ select parse_price('a.b', 2) $$ );
select throws_ok( $$ select parse_price('', 1) $$);
select throws_ok( $$ select parse_price(' ', 3) $$);
select *
from finish();
rollback;

View File

@ -0,0 +1,86 @@
-- Test set_campsite_type_cost
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'set_campsite_type_cost', array['uuid', 'integer', 'integer', 'text', 'integer']);
select function_lang_is('camper', 'set_campsite_type_cost', array['uuid', 'integer', 'integer', 'text', 'integer'], 'sql');
select function_returns('camper', 'set_campsite_type_cost', array['uuid', 'integer', 'integer', 'text', 'integer'], 'void');
select isnt_definer('camper', 'set_campsite_type_cost', array['uuid', 'integer', 'integer', 'text', 'integer']);
select volatility_is('camper', 'set_campsite_type_cost', array['uuid', 'integer', 'integer', 'text', 'integer'], 'volatile');
select function_privs_are('camper', 'set_campsite_type_cost', array ['uuid', 'integer', 'integer', 'text', 'integer'], 'guest', array[]::text[]);
select function_privs_are('camper', 'set_campsite_type_cost', array ['uuid', 'integer', 'integer', 'text', 'integer'], 'employee', array[]::text[]);
select function_privs_are('camper', 'set_campsite_type_cost', array ['uuid', 'integer', 'integer', 'text', 'integer'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'set_campsite_type_cost', array ['uuid', 'integer', 'integer', 'text', 'integer'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate campsite_type_cost cascade;
truncate season cascade;
truncate campsite_type cascade;
truncate media cascade;
truncate media_content cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into media_content (media_type, bytes)
values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
;
insert into media (media_id, company_id, original_filename, content_hash)
values (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};'))
;
insert into campsite_type (campsite_type_id, company_id, slug, media_id, name, description, max_campers, dogs_allowed, active)
values (3, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '', 5, false, true)
;
insert into season (season_id, company_id, name)
values (4, 1, 'High')
, (5, 1, 'Mid')
, (6, 1, 'Low')
;
insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights)
values (3, 4, 44, 4)
, (3, 5, 55, 5)
;
select lives_ok(
$$ select set_campsite_type_cost('87452b88-b48f-48d3-bb6c-0296de64164e', 4, 2, '12.34') $$,
'Should be able to edit the cost for high season'
);
select lives_ok(
$$ select set_campsite_type_cost('87452b88-b48f-48d3-bb6c-0296de64164e', 5, 6, '0.0') $$,
'Should be able to set the cost for mid season to zero'
);
select lives_ok(
$$ select set_campsite_type_cost('87452b88-b48f-48d3-bb6c-0296de64164e', 6, 1, '3.21') $$,
'Should be able to set the cost for low season, adding it.'
);
select bag_eq(
$$ select campsite_type_id, season_id, cost_per_night, min_nights from campsite_type_cost $$,
$$ values (3, 4, 1234, 2)
, (3, 5, 0, 6)
, (3, 6, 321, 1)
$$,
'Should have updated all campsite type costs.'
);
select *
from finish();
rollback;

46
test/to_price.sql Normal file
View File

@ -0,0 +1,46 @@
-- Test to_price
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(29);
set search_path to camper, public;
select has_function('camper', 'to_price', array ['integer', 'integer']);
select function_lang_is('camper', 'to_price', array ['integer', 'integer'], 'plpgsql');
select function_returns('camper', 'to_price', array ['integer', 'integer'], 'text');
select isnt_definer('camper', 'to_price', array ['integer', 'integer']);
select volatility_is('camper', 'to_price', array ['integer', 'integer'], 'immutable');
select function_privs_are('camper', 'to_price', array ['integer', 'integer'], 'guest', array []::text[]);
select function_privs_are('camper', 'to_price', array ['integer', 'integer'], 'employee', array ['EXECUTE']);
select function_privs_are('camper', 'to_price', array ['integer', 'integer'], 'admin', array ['EXECUTE']);
select function_privs_are('camper', 'to_price', array ['integer', 'integer'], 'authenticator', array []::text[]);
select is( to_price(0, 2), '0.00' );
select is( to_price(0, 3), '0.000' );
select is( to_price(1, 2), '0.01' );
select is( to_price(1, 3), '0.001' );
select is( to_price(-1, 2), '-0.01' );
select is( to_price(-1, 3), '-0.001' );
select is( to_price(10, 2), '0.10' );
select is( to_price(10, 3), '0.010' );
select is( to_price(100, 2), '1.00' );
select is( to_price(100, 3), '0.100' );
select is( to_price(110, 2), '1.10' );
select is( to_price(1100, 3), '1.100' );
select is( to_price(12345678, 2), '123456.78' );
select is( to_price(12345678, 3), '12345.678' );
select is( to_price(12345678, 4), '1234.5678' );
select is( to_price(12345678, 5), '123.45678' );
select is( to_price(-12345678, 2), '-123456.78' );
select is( to_price(-12345678, 3), '-12345.678' );
select is( to_price(-12345678, 4), '-1234.5678' );
select is( to_price(-12345678, 5), '-123.45678' );
select *
from finish();
rollback;

View File

@ -0,0 +1,18 @@
-- Verify camper:campsite_type_cost on pg
begin;
select campsite_type_id
, season_id
, cost_per_night
, min_nights
from camper.campsite_type_cost
where false;
select 1 / count(*) from pg_class where oid = 'camper.campsite_type_cost'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.campsite_type_cost'::regclass;
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.campsite_type_cost'::regclass;
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.campsite_type_cost'::regclass;
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.campsite_type_cost'::regclass;
rollback;

7
verify/parse_price.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify camper:parse_price on pg
begin;
select has_function_privilege('camper.parse_price(text, integer)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:set_campsite_type_cost on pg
begin;
select has_function_privilege('camper.set_campsite_type_cost(uuid, integer, integer, text, integer)', 'execute');
rollback;

7
verify/to_price.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify camper:to_price on pg
begin;
select has_function_privilege('camper.to_price(integer, integer)', 'execute');
rollback;

View File

@ -558,6 +558,24 @@ dt {
}
}
.price-list {
flex-direction: column;
gap: 0;
}
.price-list dt {
display: flex;
align-items: center;
gap: .5rem;
border: none;
padding: 0;
margin-bottom: 0;
}
.price-list dd + dt {
margin-top: 2rem;
}
.outside_activities {
margin-top: 2rem;
}

View File

@ -68,6 +68,32 @@
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Prices }}
<fieldset>
<legend>{{( pgettext "Prices" "title" )}}</legend>
{{ range . }}
<fieldset>
<legend>{{ .SeasonName }}</legend>
{{ with .PricePerNight -}}
<label>
{{( pgettext "Price per night" "input")}}<br>
<input type="number" name="{{ .Name }}" value="{{ .Val }}" min="0" step="0.01"
required {{ template "error-attrs" . }}><br>
{{ template "error-message" . }}
</label>
{{- end }}
{{ with .MinNights -}}
<label>
{{( pgettext "Minimum number of nights" "input")}}<br>
<input type="number" name="{{ .Name }}" value="{{ .Val }}" min="1"
required {{ template "error-attrs" . }}><br>
{{ template "error-message" . }}
</label>
{{- end }}
</fieldset>
{{- end }}
</fieldset>
{{- end }}
{{ with .Description -}}
<label>
{{( pgettext "Description" "input")}}<br>

View File

@ -11,4 +11,24 @@
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.publicPage*/ -}}
<h2>{{ .Name }}</h2>
{{ .Description }}
{{ with .Prices -}}
<article>
<h3>{{( pgettext "Prices" "title" )}}</h3>
<dl class="price-list">
{{ range . -}}
<dt>
<svg width="20px" height="20px">
<circle cx="50%" cy="50%" r="49%" fill="{{ .SeasonColor }}" stroke="#000" stroke-width=".5"/>
</svg>
{{ .SeasonName }}
</dt>
<dd>{{ printf (gettext "%s €/night") .PricePerNight }}</dd>
{{ if gt .MinNights 1 -}}
<dd>{{ printf (gettext "*Minimum %d nights per stay") .MinNights }}</dd>
{{- end }}
{{- end }}
</dl>
</article>
{{- end }}
{{- end }}