From 6c37031b00e89cecc84f1658539a46567da6e3da Mon Sep 17 00:00:00 2001 From: Hazem Krimi Date: Mon, 16 Jun 2025 16:41:02 +0100 Subject: [PATCH] wip: invoices api --- internal/api/api.go | 5 +- internal/api/invoice.go | 291 ++++++++++++++++++++++++++++++++++ internal/api/routes.go | 15 ++ internal/api/user.go | 2 +- internal/api/validator.go | 2 + internal/models/invoice.go | 189 ++++++++++++++++++++++ internal/types/invoice.go | 107 +++++++++++++ internal/types/user.go | 3 +- migrations/20250616102420.sql | 29 ++++ migrations/20250616150439.sql | 30 ++++ migrations/20250616151658.sql | 2 + migrations/atlas.sum | 5 +- 12 files changed, 675 insertions(+), 5 deletions(-) create mode 100644 internal/api/invoice.go create mode 100644 internal/models/invoice.go create mode 100644 internal/types/invoice.go create mode 100644 migrations/20250616102420.sql create mode 100644 migrations/20250616150439.sql create mode 100644 migrations/20250616151658.sql diff --git a/internal/api/api.go b/internal/api/api.go index 54de1ec..64016f1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -45,9 +45,10 @@ func (api *API) Initialize() { api.instance.Use(session.Middleware(sessions.NewCookieStore([]byte("SECRET")))) api.instance.Pre(middleware.AddTrailingSlash()) - api.ClientRoutes() - api.UserRoutes() api.AuthRoutes() + api.UserRoutes() + api.ClientRoutes() + api.InvoiceRoutes() api.instance.Logger.Fatal(api.instance.Start(fmt.Sprintf(":%d", lib.DEFAULT_PORT))) } diff --git a/internal/api/invoice.go b/internal/api/invoice.go new file mode 100644 index 0000000..05ba381 --- /dev/null +++ b/internal/api/invoice.go @@ -0,0 +1,291 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + + "github.com/hazemKrimi/crimson-vault/internal/types" +) + +func (api *API) CreateItemHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"Invoice ID is required to add an Invoice Item!"}} + } + + var body types.CreateItemRequestBody + + if err := context.Bind(&body); err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"Invalid JSON!"}} + } + + if err := context.Validate(body); err != nil { + return err + } + + item, err := api.db.CreateItem(userId, id, body) + + return context.JSON(http.StatusOK, item) +} + +func (api *API) CreateInvoiceHandler(context echo.Context) error { + userId, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(userId) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + var body types.CreateInvoiceRequestBody + + if err := context.Bind(&body); err != nil { + return types.Error{Code: http.StatusBadRequest, Cause: err, Messages: []string{"Invalid JSON!"}} + } + + if err := context.Validate(body); err != nil { + return err + } + + invoice, err := api.db.CreateInvoice(id, body) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error creating Invoice!"}} + } + + return context.JSON(http.StatusOK, invoice) +} + +func (api *API) GetAllItemsHandler(context echo.Context) error { + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"Invoice ID is required to get Items!"}} + } + + items, err := api.db.GetItems(id) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Messages: []string{"Unexpected error getting Items!"}} + } + + return context.JSON(http.StatusOK, items) +} + +func (api *API) GetAllInvoicesHandler(context echo.Context) error { + invoices, err := api.db.GetInvoices() + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting Invoices!"}} + } + + return context.JSON(http.StatusOK, invoices) +} + +func (api *API) GetItemHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to get an Invoice Item!"}} + } + + var item types.Item + + if err := api.db.GetItemById(userId, id, &item); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice Item not found!"}} + } + + return context.JSON(http.StatusOK, item) +} + +func (api *API) GetInvoiceHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to get an Invoice!"}} + } + + var invoice types.Invoice + + if err := api.db.GetInvoiceById(userId, id, &invoice); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice not found!"}} + } + + return context.JSON(http.StatusOK, invoice) +} + +func (api *API) UpdateItemHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to update an Invoice Item!"}} + } + + var body types.UpdateItemRequestBody + + if err := context.Bind(&body); err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"Invalid JSON!"}} + } + + empty := body == types.UpdateItemRequestBody{} + + if empty { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"You must update at lease one field!"}} + } + + var item types.Item + + if err := api.db.UpdateItem(userId, id, body, &item); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice Item not found!"}} + } + + return context.JSON(http.StatusOK, item) +} + +func (api *API) UpdateInvoiceHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to update an Invoice!"}} + } + + var body types.UpdateInvoiceRequestBody + + if err := context.Bind(&body); err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"Invalid JSON!"}} + } + + empty := body == types.UpdateInvoiceRequestBody{} + + if empty { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"You must update at lease one field!"}} + } + + var item types.Item + + if err := api.db.UpdateInvoice(userId, id, body, &item); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice not found!"}} + } + + return context.JSON(http.StatusOK, item) +} + +func (api *API) DeleteItemHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to delete an Invoice Item!"}} + } + + if err := api.db.DeleteItem(userId, id); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice Item not found!"}} + } + + return context.JSON(http.StatusOK, map[string]string{"message": "Invoice Item deleted successfully!"}) +} + +func (api *API) DeleteInvoiceHandler(context echo.Context) error { + userIdString, ok := context.Get("id").(string) + + if !ok { + return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}} + } + + userId, err := uuid.Parse(userIdString) + + if err != nil { + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + } + + id, err := uuid.Parse(context.Param("id")) + + if err != nil { + return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to delete an Invoice Item!"}} + } + + if err := api.db.DeleteInvoice(userId, id); err != nil { + return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice Item not found!"}} + } + + return context.JSON(http.StatusOK, map[string]string{"message": "Invoice Item deleted successfully!"}) +} diff --git a/internal/api/routes.go b/internal/api/routes.go index f1cabb3..6fd9318 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -25,6 +25,21 @@ func (api *API) UserRoutes() { users.DELETE("/me/logo/", api.DeleteUserLogoHandler, api.AuthSessionMiddleware) } +func (api *API) InvoiceRoutes() { + invoices := api.instance.Group("/api/invoices", api.AuthSessionMiddleware) + + invoices.GET("/", api.GetAllInvoicesHandler) + invoices.POST("/", api.CreateInvoiceHandler) + invoices.POST("/:id/items/", api.CreateItemHandler) + invoices.GET("/:id/", api.GetInvoiceHandler) + invoices.GET("/:id/items/", api.GetAllItemsHandler) + invoices.GET("/items/:id/", api.GetItemHandler) + invoices.PUT("/:id/", api.UpdateInvoiceHandler) + invoices.PUT("/items/:id/", api.UpdateItemHandler) + invoices.DELETE("/:id/", api.DeleteInvoiceHandler) + invoices.DELETE("/items/:id/", api.DeleteItemHandler) +} + func (api *API) AuthRoutes() { auth := api.instance.Group("/api/auth") diff --git a/internal/api/user.go b/internal/api/user.go index d2675ba..410c1b0 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -51,7 +51,7 @@ func (api *API) GetAllUsersHandler(context echo.Context) error { users, err := api.db.GetUsers() if err != nil { - return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting User!"}} + return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error getting Users!"}} } return context.JSON(http.StatusOK, users) diff --git a/internal/api/validator.go b/internal/api/validator.go index 79856db..0ac4258 100644 --- a/internal/api/validator.go +++ b/internal/api/validator.go @@ -34,6 +34,8 @@ func (v *CustomValidator) Validate(i any) error { msg = fmt.Sprintf("%s must only contain alphabetic characters!", field) case "e164": msg = fmt.Sprintf("%s must be a valid phone number in e164 format!", field) + case "iso4217": + msg = fmt.Sprintf("%s must be a valid currency in iso4217 format!", field) case "password": msg = fmt.Sprintf("%s must have at lease one uppercase, one lowercase, one number and one special character!", field) case "eqcsfield": diff --git a/internal/models/invoice.go b/internal/models/invoice.go new file mode 100644 index 0000000..9d8069d --- /dev/null +++ b/internal/models/invoice.go @@ -0,0 +1,189 @@ +package models + +import ( + "time" + + "github.com/google/uuid" + + "github.com/hazemKrimi/crimson-vault/internal/types" +) + +func (db *DB) CreateItem(userId, invoiceId uuid.UUID, body types.CreateItemRequestBody) (types.Item, error) { + item := types.Item{ + ID: uuid.New().String(), + InvoiceID: invoiceId.String(), + UserID: userId.String(), + Name: body.Name, + Type: body.Type, + Quantity: body.Quantity, + Tax: body.Tax, + } + + result := db.instance.Create(&item) + + if result.Error != nil { + return types.Item{}, result.Error + } + + return item, nil +} + +func (db *DB) CreateInvoice(userId uuid.UUID, body types.CreateInvoiceRequestBody) (types.Invoice, error) { + dueAt, err := time.Parse("2006-01-02T15:04:05Z", body.DueAt) + + if err != nil { + return types.Invoice{}, err + } + + invoice := types.Invoice{ + ID: uuid.New().String(), + UserID: userId.String(), + ClientID: body.ClientID, + DueAt: dueAt, + Currency: body.Currency, + VAT: body.VAT, + Status: types.Draft.String(), + } + + result := db.instance.Create(&invoice) + + if result.Error != nil { + return types.Invoice{}, result.Error + } + + var items []types.Item + + for _, invoiceItem := range body.Items { + invoiceId, err := uuid.Parse(invoice.ID) + + if err != nil { + return types.Invoice{}, err + } + + item, err := db.CreateItem(userId, invoiceId, invoiceItem) + + if err != nil { + return types.Invoice{}, err + } + + items = append(items, item) + } + + result = db.instance.Model(&invoice).Updates(types.Invoice{ + Items: invoice.Items, + }) + + if result.Error != nil { + return types.Invoice{}, result.Error + } + + return invoice, nil +} + +func (db *DB) GetItems(userId, id uuid.UUID) ([]types.Item, error) { + var items []types.Item + + result := db.instance.Model(&types.Item{}).Where("user_id = ?", userId).Where("invoice_id = ?", id).Find(&items) + + if result.Error != nil { + return nil, result.Error + } + + return items, nil +} + +func (db *DB) GetInvoices(userId uuid.UUID) ([]types.Invoice, error) { + var invoices []types.Invoice + + result := db.instance.Model(&types.Invoice{}).Where("user_id = ?", userId).Preload("Items").Find(&invoices) + + if result.Error != nil { + return nil, result.Error + } + + return invoices, nil +} + +func (db *DB) GetItemById(userId, id uuid.UUID, item *types.Item) error { + result := db.instance.Model(&types.Item{}).Where("user_id = ?", userId).Where("id = ?", id).First(item) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) GetInvoiceById(userId, id uuid.UUID, invoice *types.Invoice) error { + result := db.instance.Model(&types.Invoice{}).Preload("Items").Where("user_id = ?", userId).Where("id = ?", id).First(invoice) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateItem(userId, id uuid.UUID, body types.UpdateItemRequestBody, item *types.Item) error { + result := db.instance.Where("user_id = ?", userId).Where("id = ?", id).First(item) + + if result.Error != nil { + return result.Error + } + + result = db.instance.Model(item).Updates(types.Item{ + Name: body.Name, + Type: body.Type, + Quantity: body.Quantity, + Tax: body.Tax, + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateInvoice(userId, id uuid.UUID, body types.UpdateInvoiceRequestBody, invoice *types.Item) error { + result := db.instance.Where("user_id = ?", userId).Where("id = ?", id).First(invoice) + + if result.Error != nil { + return result.Error + } + + dueAt, err := time.Parse("2006-01-02T15:04:05.000Z", body.DueAt) + + if err != nil { + return err + } + + result = db.instance.Model(invoice).Updates(types.Invoice{ + DueAt: dueAt, + Currency: body.Currency, + VAT: body.VAT, + Status: body.Status, + }) + + return nil +} + +func (db *DB) DeleteItem(userId, id uuid.UUID) error { + result := db.instance.Unscoped().Where("user_id = ?", userId).Where("id = ?", id).Delete(&types.Item{}) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) DeleteInvoice(userId, id uuid.UUID) error { + result := db.instance.Unscoped().Where("user_id = ?", userId).Where("id = ?", id).Delete(&types.Invoice{}) + + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/internal/types/invoice.go b/internal/types/invoice.go new file mode 100644 index 0000000..e5cbaef --- /dev/null +++ b/internal/types/invoice.go @@ -0,0 +1,107 @@ +package types + +import ( + "time" + + "gorm.io/gorm" +) + +type ItemType int + +const ( + Service ItemType = iota + Product +) + +func (itemType ItemType) String() string { + switch itemType { + case Service: + return "service" + case Product: + return "product" + default: + return "unknown" + } +} + +type Item struct { + ID string `json:"id" gorm:"type:varchar(255);primaryKey"` + InvoiceID string `json:"invoiceId" gorm:"type:varchar(255)"` + UserID string `json:"userId" gorm:"type:varchar(255)"` + Name string `json:"name"` + Type string `json:"type"` + Price uint32 `json:"price"` + Quantity uint32 `json:"quantity"` + Tax uint32 `json:"tax"` +} + +type Status int + +const ( + Draft Status = iota + Posted + Paid + Late +) + +func (status Status) String() string { + switch status { + case Draft: + return "draft" + case Posted: + return "posted" + case Paid: + return "paid" + case Late: + return "late" + default: + return "unknown" + } +} + +type Invoice struct { + ID string `json:"id" gorm:"type:varchar(255);primaryKey"` + UserID string `json:"userId" gorm:"type:varchar(255)"` + ClientID string `json:"clientId" gorm:"type:varchar(255)"` + CreatedAt time.Time `json:"createAt"` + DueAt time.Time `json:"dueAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"deletedAt" gorm:"index"` + Reference string `json:"reference"` + Status string `json:"status"` + PDF string `json:"pdf"` + Currency string `json:"currency"` + VAT uint32 `json:"vat"` + Items []Item `json:"items"` +} + +type CreateItemRequestBody struct { + Name string `json:"name" validate:"alpha,required"` + Type string `json:"type" validate:"alpha,required"` + Price uint32 `json:"price" validate:"number,required"` + Quantity uint32 `json:"quantity" validate:"number,required"` + Tax uint32 `json:"tax" validate:"number,omitempty"` +} + +type UpdateItemRequestBody struct { + Name string `json:"name" validate:"alpha,omitempty"` + Type string `json:"type" validate:"alpha,omitempty"` + Price uint32 `json:"price" validate:"number,omitempty"` + Quantity uint32 `json:"quantity" validate:"number,omitempty"` + Tax uint32 `json:"tax" validate:"number,omitempty"` +} + +type CreateInvoiceRequestBody struct { + ClientID string `json:"clientId" validate:"uuid4,required"` + DueAt string `json:"dueAt" validate:"datetime=2006-01-02T15:04:05Z,required"` + Currency string `json:"currency" validate:"iso4217,required"` + VAT uint32 `json:"vat" validate:"number,required"` + Items []CreateItemRequestBody `json:"items"` +} + +type UpdateInvoiceRequestBody struct { + DueAt string `json:"dueAt" validte:"datetime=2006-01-02T15:04:05Z,omitempty"` + Currency string `json:"currency" validate:"iso4217,omitempty"` + VAT uint32 `json:"vat" validate:"number,omitempty"` + Status string `json:"status" validate:"alpha,omitempty"` +} diff --git a/internal/types/user.go b/internal/types/user.go index 049f557..3862a1e 100644 --- a/internal/types/user.go +++ b/internal/types/user.go @@ -22,7 +22,7 @@ type User struct { Email string `json:"email"` Username string `json:"username" gorm:"unique"` Password string `json:"-"` - Clients []Client `json:"clients" gorm:"constraint:onDelete:CASCADE"` + Clients []Client `json:"clients" gorm:"constraint:onDelete:CASCADE"` } type CreateUserRequestBody struct { @@ -33,6 +33,7 @@ type CreateUserRequestBody struct { Country string `json:"country" validate:"required,alpha"` Phone string `json:"phone" validate:"required,e164"` Email string `json:"email" validate:"required,email"` + Username string `json:"username" validate:"required"` } type UpdateUserRequestBody struct { diff --git a/migrations/20250616102420.sql b/migrations/20250616102420.sql new file mode 100644 index 0000000..cd567e5 --- /dev/null +++ b/migrations/20250616102420.sql @@ -0,0 +1,29 @@ +-- Create "invoices" table +CREATE TABLE `invoices` ( + `id` varchar NULL, + `user_id` varchar NULL, + `client_id` varchar NULL, + `created_at` datetime NULL, + `due_at` datetime NULL, + `updated_at` datetime NULL, + `deleted_at` datetime NULL, + `reference` text NULL, + `status` text NULL, + `pdf` text NULL, + `currency` text NULL, + `vat` text NULL, + PRIMARY KEY (`id`) +); +-- Create index "idx_invoices_deleted_at" to table: "invoices" +CREATE INDEX `idx_invoices_deleted_at` ON `invoices` (`deleted_at`); +-- Create "items" table +CREATE TABLE `items` ( + `id` varchar NULL, + `invoice_id` varchar NULL, + `name` text NULL, + `type` text NULL, + `quantity` integer NULL, + `tax` integer NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_invoices_items` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION +); diff --git a/migrations/20250616150439.sql b/migrations/20250616150439.sql new file mode 100644 index 0000000..5fde0af --- /dev/null +++ b/migrations/20250616150439.sql @@ -0,0 +1,30 @@ +-- Disable the enforcement of foreign-keys constraints +PRAGMA foreign_keys = off; +-- Create "new_invoices" table +CREATE TABLE `new_invoices` ( + `id` varchar NULL, + `user_id` varchar NULL, + `client_id` varchar NULL, + `created_at` datetime NULL, + `due_at` datetime NULL, + `updated_at` datetime NULL, + `deleted_at` datetime NULL, + `reference` text NULL, + `status` text NULL, + `pdf` text NULL, + `currency` text NULL, + `vat` integer NULL, + PRIMARY KEY (`id`) +); +-- Copy rows from old table "invoices" to new temporary table "new_invoices" +INSERT INTO `new_invoices` (`id`, `user_id`, `client_id`, `created_at`, `due_at`, `updated_at`, `deleted_at`, `reference`, `status`, `pdf`, `currency`, `vat`) SELECT `id`, `user_id`, `client_id`, `created_at`, `due_at`, `updated_at`, `deleted_at`, `reference`, `status`, `pdf`, `currency`, `vat` FROM `invoices`; +-- Drop "invoices" table after copying rows +DROP TABLE `invoices`; +-- Rename temporary table "new_invoices" to "invoices" +ALTER TABLE `new_invoices` RENAME TO `invoices`; +-- Create index "idx_invoices_deleted_at" to table: "invoices" +CREATE INDEX `idx_invoices_deleted_at` ON `invoices` (`deleted_at`); +-- Add column "user_id" to table: "items" +ALTER TABLE `items` ADD COLUMN `user_id` varchar NULL; +-- Enable back the enforcement of foreign-keys constraints +PRAGMA foreign_keys = on; diff --git a/migrations/20250616151658.sql b/migrations/20250616151658.sql new file mode 100644 index 0000000..89770a1 --- /dev/null +++ b/migrations/20250616151658.sql @@ -0,0 +1,2 @@ +-- Add column "price" to table: "items" +ALTER TABLE `items` ADD COLUMN `price` integer NULL; diff --git a/migrations/atlas.sum b/migrations/atlas.sum index 44c080f..d171957 100644 --- a/migrations/atlas.sum +++ b/migrations/atlas.sum @@ -1,2 +1,5 @@ -h1:2049NzfhHw8DDKsmQejeOcETj2pojlv5sA+O/QhmtG0= +h1:dDD/MWKqjFIkCAuR4i/9LtH4ibg9wZR03FApuHHYCos= 20250611102124.sql h1:HkDgtWXUxfAR9gIObTMzP98pVEIakerASGGSufW695k= +20250616102420.sql h1:7C1kEskaDwdHmQOu3t48oZcky0WaNiFeIugJcJFkjKM= +20250616150439.sql h1:nx8CvH5om7lbeEezo7roNcTV+f0agch0gBjYxKtTDn8= +20250616151658.sql h1:O4hbYFj4bwMDML0mR9PtSn7GUbXQVCbh+lGkwTNBwnU=