Generate the week's Supply run from its plan
Add internal/shopping with Generate(weekStart), one entry point for the first generation and every regeneration. It builds one group per plan day (dinner_id, date, position by date) and one item per ingredient of the dinner's recipe, in ingredient order, reusing the values recipe grounding stored: a fetched recipe's normalized name with qty and unit joined for display, or a manual recipe's line as typed with no qty. A day whose dinner has no recipe or no ingredients gets no group and is reported in Skipped. Regenerating keeps the id and needed flag of each item whose (dinner_id, name) is unchanged, matching repeated names in order; new items are needed and items no longer planned are removed. It is deterministic and makes no model call. A frozen list is refused. Migration 0016 makes shopping_lists.title nullable (never empty) so a list exists untitled until the AI titles it, and adds shopping_groups.date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -258,6 +258,7 @@ type ShoppingGroup struct {
|
||||
ID string
|
||||
ListID string
|
||||
DinnerID string
|
||||
Date string
|
||||
Position int64
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
@@ -278,7 +279,7 @@ type ShoppingList struct {
|
||||
ID string
|
||||
WeekStart string
|
||||
PlanID string
|
||||
Title string
|
||||
Title sql.NullString
|
||||
Status string
|
||||
SentToChatAt sql.NullString
|
||||
FrozenAt sql.NullString
|
||||
|
||||
@@ -43,6 +43,11 @@ type Querier interface {
|
||||
CreateRecipeIngredient(ctx context.Context, arg CreateRecipeIngredientParams) error
|
||||
CreateRecipeStep(ctx context.Context, arg CreateRecipeStepParams) error
|
||||
CreateRotationReview(ctx context.Context, arg CreateRotationReviewParams) error
|
||||
CreateShoppingGroup(ctx context.Context, arg CreateShoppingGroupParams) error
|
||||
// A new item is needed until the household marks it "have it".
|
||||
CreateShoppingItem(ctx context.Context, arg CreateShoppingItemParams) error
|
||||
// A new, untitled Supply run: the title is written later, by the AI.
|
||||
CreateShoppingList(ctx context.Context, arg CreateShoppingListParams) error
|
||||
// name is the name the dish is suggested under, kept whatever its dinner is
|
||||
// later renamed.
|
||||
CreateSuggestion(ctx context.Context, arg CreateSuggestionParams) error
|
||||
@@ -59,6 +64,8 @@ type Querier interface {
|
||||
DeleteIntegrationsGoogle(ctx context.Context) error
|
||||
DeleteRecipeIngredients(ctx context.Context, recipeID string) error
|
||||
DeleteRecipeSteps(ctx context.Context, recipeID string) error
|
||||
DeleteShoppingGroup(ctx context.Context, id string) error
|
||||
DeleteShoppingItem(ctx context.Context, id string) error
|
||||
FailJob(ctx context.Context, arg FailJobParams) (int64, error)
|
||||
// Fails every unarchived dinner still pending whose grounding job for its
|
||||
// current grounding_revision is no longer queued or running: the process
|
||||
@@ -113,6 +120,9 @@ type Querier interface {
|
||||
GetRecipeBySourceURL(ctx context.Context, sourceUrl string) (Recipe, error)
|
||||
// Returns sql.ErrNoRows until the row exists.
|
||||
GetRecipeSearchSettings(ctx context.Context) (RecipeSearchSetting, error)
|
||||
// The Supply run of the week starting week_start. Returns sql.ErrNoRows
|
||||
// when there is none.
|
||||
GetShoppingList(ctx context.Context, weekStart string) (ShoppingList, error)
|
||||
// A suggestion with its dinner. Returns sql.ErrNoRows for an unknown id.
|
||||
GetSuggestion(ctx context.Context, id string) (GetSuggestionRow, error)
|
||||
// The id of the job that runs batch batch_id.
|
||||
@@ -164,6 +174,11 @@ type Querier interface {
|
||||
ListRecentPassedNames(ctx context.Context, limit int64) ([]string, error)
|
||||
ListRecipeIngredients(ctx context.Context, recipeID string) ([]RecipeIngredient, error)
|
||||
ListRecipeSteps(ctx context.Context, recipeID string) ([]RecipeStep, error)
|
||||
// The groups of list list_id, in list order.
|
||||
ListShoppingGroups(ctx context.Context, listID string) ([]ShoppingGroup, error)
|
||||
// The items of list list_id with their group's dinner, in list order: by
|
||||
// group, then by item.
|
||||
ListShoppingItems(ctx context.Context, listID string) ([]ListShoppingItemsRow, error)
|
||||
// The ids of the dinners with status quest_log planned on date, on a plan
|
||||
// of any status, that have no Rotation check for date yet.
|
||||
ListTriedCandidates(ctx context.Context, date string) ([]string, error)
|
||||
@@ -191,6 +206,10 @@ type Querier interface {
|
||||
// becomes synced. A plan edited since (another revision) is left as it is
|
||||
// (0 rows).
|
||||
MarkWeekPlanSynced(ctx context.Context, arg MarkWeekPlanSyncedParams) (int64, error)
|
||||
MoveShoppingGroup(ctx context.Context, arg MoveShoppingGroupParams) error
|
||||
// Places item id in group group_id at position with quantity qty; its
|
||||
// name and needed flag are kept.
|
||||
MoveShoppingItem(ctx context.Context, arg MoveShoppingItemParams) error
|
||||
// Moves a dinner still suggested to status (quest_log or archived),
|
||||
// recording updated_by; a dinner no longer suggested is left as it is
|
||||
// (0 rows).
|
||||
@@ -208,6 +227,8 @@ type Querier interface {
|
||||
// Records a new draft over plan id: its status and note, raising its
|
||||
// revision.
|
||||
RedraftWeekPlan(ctx context.Context, arg RedraftWeekPlanParams) error
|
||||
// Records a generation of list id from plan plan_id.
|
||||
RegenerateShoppingList(ctx context.Context, arg RegenerateShoppingListParams) error
|
||||
// An unknown id returns sql.ErrNoRows.
|
||||
SetDinnerFavorite(ctx context.Context, arg SetDinnerFavoriteParams) (Dinner, error)
|
||||
// Sets the chosen calendar; affects no row when nothing is connected.
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: shopping.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createShoppingGroup = `-- name: CreateShoppingGroup :exec
|
||||
INSERT INTO shopping_groups (id, list_id, dinner_id, date, position, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type CreateShoppingGroupParams struct {
|
||||
ID string
|
||||
ListID string
|
||||
DinnerID string
|
||||
Date string
|
||||
Position int64
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateShoppingGroup(ctx context.Context, arg CreateShoppingGroupParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createShoppingGroup,
|
||||
arg.ID,
|
||||
arg.ListID,
|
||||
arg.DinnerID,
|
||||
arg.Date,
|
||||
arg.Position,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const createShoppingItem = `-- name: CreateShoppingItem :exec
|
||||
INSERT INTO shopping_items (id, group_id, name, qty, needed, position, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
||||
`
|
||||
|
||||
type CreateShoppingItemParams struct {
|
||||
ID string
|
||||
GroupID string
|
||||
Name string
|
||||
Qty sql.NullString
|
||||
Position int64
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
// A new item is needed until the household marks it "have it".
|
||||
func (q *Queries) CreateShoppingItem(ctx context.Context, arg CreateShoppingItemParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createShoppingItem,
|
||||
arg.ID,
|
||||
arg.GroupID,
|
||||
arg.Name,
|
||||
arg.Qty,
|
||||
arg.Position,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const createShoppingList = `-- name: CreateShoppingList :exec
|
||||
INSERT INTO shopping_lists (id, week_start, plan_id, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 'current', ?, ?)
|
||||
`
|
||||
|
||||
type CreateShoppingListParams struct {
|
||||
ID string
|
||||
WeekStart string
|
||||
PlanID string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
// A new, untitled Supply run: the title is written later, by the AI.
|
||||
func (q *Queries) CreateShoppingList(ctx context.Context, arg CreateShoppingListParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createShoppingList,
|
||||
arg.ID,
|
||||
arg.WeekStart,
|
||||
arg.PlanID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteShoppingGroup = `-- name: DeleteShoppingGroup :exec
|
||||
DELETE FROM shopping_groups
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteShoppingGroup(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteShoppingGroup, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteShoppingItem = `-- name: DeleteShoppingItem :exec
|
||||
DELETE FROM shopping_items
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteShoppingItem(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteShoppingItem, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getShoppingList = `-- name: GetShoppingList :one
|
||||
SELECT id, week_start, plan_id, title, status, sent_to_chat_at, frozen_at, created_at, updated_at FROM shopping_lists
|
||||
WHERE week_start = ?
|
||||
`
|
||||
|
||||
// The Supply run of the week starting week_start. Returns sql.ErrNoRows
|
||||
// when there is none.
|
||||
func (q *Queries) GetShoppingList(ctx context.Context, weekStart string) (ShoppingList, error) {
|
||||
row := q.db.QueryRowContext(ctx, getShoppingList, weekStart)
|
||||
var i ShoppingList
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WeekStart,
|
||||
&i.PlanID,
|
||||
&i.Title,
|
||||
&i.Status,
|
||||
&i.SentToChatAt,
|
||||
&i.FrozenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listShoppingGroups = `-- name: ListShoppingGroups :many
|
||||
SELECT id, list_id, dinner_id, date, position, created_at, updated_at FROM shopping_groups
|
||||
WHERE list_id = ?
|
||||
ORDER BY position, id
|
||||
`
|
||||
|
||||
// The groups of list list_id, in list order.
|
||||
func (q *Queries) ListShoppingGroups(ctx context.Context, listID string) ([]ShoppingGroup, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listShoppingGroups, listID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ShoppingGroup
|
||||
for rows.Next() {
|
||||
var i ShoppingGroup
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ListID,
|
||||
&i.DinnerID,
|
||||
&i.Date,
|
||||
&i.Position,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listShoppingItems = `-- name: ListShoppingItems :many
|
||||
SELECT shopping_items.id, shopping_items.group_id, shopping_items.name, shopping_items.qty, shopping_items.needed, shopping_items.position, shopping_items.created_at, shopping_items.updated_at, shopping_groups.dinner_id
|
||||
FROM shopping_items
|
||||
JOIN shopping_groups ON shopping_groups.id = shopping_items.group_id
|
||||
WHERE shopping_groups.list_id = ?
|
||||
ORDER BY shopping_groups.position, shopping_items.position, shopping_items.id
|
||||
`
|
||||
|
||||
type ListShoppingItemsRow struct {
|
||||
ShoppingItem ShoppingItem
|
||||
DinnerID string
|
||||
}
|
||||
|
||||
// The items of list list_id with their group's dinner, in list order: by
|
||||
// group, then by item.
|
||||
func (q *Queries) ListShoppingItems(ctx context.Context, listID string) ([]ListShoppingItemsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listShoppingItems, listID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListShoppingItemsRow
|
||||
for rows.Next() {
|
||||
var i ListShoppingItemsRow
|
||||
if err := rows.Scan(
|
||||
&i.ShoppingItem.ID,
|
||||
&i.ShoppingItem.GroupID,
|
||||
&i.ShoppingItem.Name,
|
||||
&i.ShoppingItem.Qty,
|
||||
&i.ShoppingItem.Needed,
|
||||
&i.ShoppingItem.Position,
|
||||
&i.ShoppingItem.CreatedAt,
|
||||
&i.ShoppingItem.UpdatedAt,
|
||||
&i.DinnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const moveShoppingGroup = `-- name: MoveShoppingGroup :exec
|
||||
UPDATE shopping_groups
|
||||
SET position = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type MoveShoppingGroupParams struct {
|
||||
Position int64
|
||||
UpdatedAt string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) MoveShoppingGroup(ctx context.Context, arg MoveShoppingGroupParams) error {
|
||||
_, err := q.db.ExecContext(ctx, moveShoppingGroup, arg.Position, arg.UpdatedAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const moveShoppingItem = `-- name: MoveShoppingItem :exec
|
||||
UPDATE shopping_items
|
||||
SET group_id = ?, qty = ?, position = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type MoveShoppingItemParams struct {
|
||||
GroupID string
|
||||
Qty sql.NullString
|
||||
Position int64
|
||||
UpdatedAt string
|
||||
ID string
|
||||
}
|
||||
|
||||
// Places item id in group group_id at position with quantity qty; its
|
||||
// name and needed flag are kept.
|
||||
func (q *Queries) MoveShoppingItem(ctx context.Context, arg MoveShoppingItemParams) error {
|
||||
_, err := q.db.ExecContext(ctx, moveShoppingItem,
|
||||
arg.GroupID,
|
||||
arg.Qty,
|
||||
arg.Position,
|
||||
arg.UpdatedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const regenerateShoppingList = `-- name: RegenerateShoppingList :exec
|
||||
UPDATE shopping_lists
|
||||
SET plan_id = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type RegenerateShoppingListParams struct {
|
||||
PlanID string
|
||||
UpdatedAt string
|
||||
ID string
|
||||
}
|
||||
|
||||
// Records a generation of list id from plan plan_id.
|
||||
func (q *Queries) RegenerateShoppingList(ctx context.Context, arg RegenerateShoppingListParams) error {
|
||||
_, err := q.db.ExecContext(ctx, regenerateShoppingList, arg.PlanID, arg.UpdatedAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user