Add the campsite type cost per season
This commit is contained in:
parent
680d51e704
commit
ef6a8f5aee
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
74
po/ca.po
74
po/ca.po
|
@ -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 l’entorn"
|
|||
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 d’allotjament"
|
||||
|
||||
#: 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 d’allotjament escollit no és vàlid."
|
||||
|
|
74
po/es.po
74
po/es.po
|
@ -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."
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:campsite_type_cost from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists camper.campsite_type_cost;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:parse_price from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.parse_price(text, integer);
|
||||
|
||||
commit;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:to_price from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists camper.to_price(integer, integer);
|
||||
|
||||
commit;
|
|
@ -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 season’s 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
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:parse_price on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.parse_price(text, integer)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify camper:to_price on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('camper.to_price(integer, integer)', 'execute');
|
||||
|
||||
rollback;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
Loading…
Reference in New Issue