commit 287eb2f2058a5b2710476cc02c483e16ed7ca1d7 Author: nochill Date: Sun Mar 5 23:35:41 2023 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..30a0f99 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.tabSize": 2, + "editor.minimap.enabled": false +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8f3f69 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +include .env + +migrateup: + migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose up + +migratedown: + migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose down + +sqlc: + sqlc generate + +test: + go test -v -cover ./... + +.PHONY: migrateup migratedown sqlc \ No newline at end of file diff --git a/db/migrations/000001_init_schema.down.sql b/db/migrations/000001_init_schema.down.sql new file mode 100644 index 0000000..e6b4118 --- /dev/null +++ b/db/migrations/000001_init_schema.down.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS merchants; +DROP TABLE IF EXISTS suppliers; +DROP TABLE IF EXISTS customers; +DROP TABLE IF EXISTS products; +DROP TABLE IF EXISTS purchase_order; +DROP TABLE IF EXISTS purchase_order_detail; +DROP TABLE IF EXISTS sale_order; +DROP TABLE IF EXISTS sale_order_detail; \ No newline at end of file diff --git a/db/migrations/000001_init_schema.up.sql b/db/migrations/000001_init_schema.up.sql new file mode 100644 index 0000000..6de022e --- /dev/null +++ b/db/migrations/000001_init_schema.up.sql @@ -0,0 +1,145 @@ +CREATE TABLE users( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "email" varchar unique not null, + "password" varchar not null, + "fullname" varchar, + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); + +CREATE TABLE merchants ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "name" varchar not null, + "owner_id" uuid references "users"("id") not null, + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +create table suppliers ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "merchant_id" uuid references "merchants"("id") not null, + "name" varchar(100) not null, + "detail" jsonb, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +CREATE TABLE customers ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "merchant_id" uuid references "merchants"("id") not null, + "name" varchar not null, + "detail" jsonb, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +CREATE TABLE products ( + "id" uuid default gen_random_uuid() primary key not null, + "merchant_id" uuid references "merchants"("id") not null, + "index_id" bigserial not null, + "name" varchar not null, + "selling_price" double precision default(0::double precision) NOT NULL, + "purchase_price" double precision default(0:: double precision) NOT NULL, + "stock" double precision default(0::double precision) NOT NULL, + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +CREATE TABLE purchase_order ( + "id" uuid default gen_random_uuid() primary key not null, + "supplier_id" uuid references "suppliers"("id") not null, + "merchant_id" uuid references "merchants"("id") not null, + "index_id" bigserial not null, + "code" varchar(100), + "is_paid" boolean not null, + "total" double precision not null, + "paid_nominal" double precision not null, + "note" text, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); + +CREATE TABLE purchase_order_detail ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "code" text, + "merchant_id" uuid references "merchants"("id") not null, + "purchase_order_id" uuid references "purchase_order"("id") not null, + "product_id" uuid references "products"("id") not null, + "quantity" double precision not null, + "sub_total" double precision not null, + "product_price" double precision not null, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +CREATE TABLE sale_order ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "code" text, + "merchant_id" uuid references "merchants"("id") not null, + "customer_id" uuid references "customers"("id"), + "is_paid" boolean, + "total" double precision not null, + "paid_nominal" double precision not null, + "note" text, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); +CREATE TABLE sale_order_detail ( + "id" uuid default gen_random_uuid() primary key not null, + "index_id" bigserial not null, + "sale_order_id" uuid references "sale_order"("id") not null, + "product_id" uuid references "products"("id") not null, + "product_name" varchar not null, + "quantity" double precision not null, + "sub_total" double precision not null, + "product_price" double precision not null, + "profit" double precision not null, + + "created_at" timestamp default(now()), + "updated_at" timestamp default(now()) +); + +CREATE INDEX ON "users"("index_id"); + + + +CREATE INDEX ON "merchants"("index_id"); + + + +CREATE INDEX ON "suppliers"("index_id"); + + + +CREATE INDEX ON "customers"("index_id"); + + + +CREATE INDEX ON "products" ("name"); +CREATE INDEX ON "products" ("selling_price"); +CREATE INDEX ON "products" ("index_id"); +CREATE INDEX ON "products" ("purchase_price"); +CREATE INDEX ON "products" ("stock"); + + +CREATE INDEX ON "purchase_order" ("merchant_id"); +CREATE INDEX ON "purchase_order" ("supplier_id"); +CREATE INDEX ON "purchase_order" ("index_id"); +CREATE INDEX ON "purchase_order" ("created_at"); + + + +CREATE INDEX ON "purchase_order_detail" ("index_id"); + + +CREATE INDEX ON "sale_order" ("index_id"); + + + +CREATE INDEX ON "sale_order_detail" ("index_id"); \ No newline at end of file diff --git a/db/query/customers.sql b/db/query/customers.sql new file mode 100644 index 0000000..930c68e --- /dev/null +++ b/db/query/customers.sql @@ -0,0 +1,23 @@ +-- name: CreateCustomers :one +INSERT INTO customers ( + merchant_id, + name, + detail +) VALUES ( + $1, $2, $3 +) +RETURNING *; + +-- name: CustomersList :many +SELECT * FROM customers +WHERE merchant_id = $1 +ORDER BY index_id +LIMIT $2 +OFFSET $3; + +-- name: UpdateCustomer :one +SELECT * FROM customers +WHERE id = $1; + +-- name: DeleteCustomer :exec +DELETE FROM customers where id = $1; \ No newline at end of file diff --git a/db/query/merchant.sql b/db/query/merchant.sql new file mode 100644 index 0000000..61fde98 --- /dev/null +++ b/db/query/merchant.sql @@ -0,0 +1,8 @@ +-- name: GetMerchantById :one +SELECT * FROM merchants +WHERE id = $1; + +-- name: GetMerchantByUserId :one +SELECT * FROM merchants +WHERE owner_id = $1; + diff --git a/db/query/products.sql b/db/query/products.sql new file mode 100644 index 0000000..44aa217 --- /dev/null +++ b/db/query/products.sql @@ -0,0 +1,36 @@ +-- name: CreateProduct :one +INSERT INTO products ( + merchant_id, + name, + selling_price, + purchase_price, + stock +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING *; + +-- name: UpdateProductStock :one +UPDATE products +SET stock = $1 +WHERE id = $2 +RETURNING stock; + +-- name: GetProduct :one +SELECT * FROM products +WHERE id = $1; + +-- name: ListProducts :many +SELECT * FROM products +ORDER BY index_id +LIMIT $1 +OFFSET $2; + +-- name: UpdateProduct :one +UPDATE products +SET name = $2, selling_price = $3, purchase_price = $4, stock = $5, updated_at = $6 +WHERE id = $1 +RETURNING *; + +-- name: DeleteProduct :exec +DELETE FROM products WHERE id = $1; \ No newline at end of file diff --git a/db/query/purchase_order.sql b/db/query/purchase_order.sql new file mode 100644 index 0000000..ee7c5f1 --- /dev/null +++ b/db/query/purchase_order.sql @@ -0,0 +1,13 @@ +-- name: CreatePurchaseOrder :one +INSERT INTO purchase_order ( + merchant_id, + supplier_id, + code, + is_paid, + total, + paid_nominal, + note +) VALUES ( + $1, $2, $3, $4, $5, $6, $7 +) +RETURNING *; \ No newline at end of file diff --git a/db/query/purchase_order_detail.sql b/db/query/purchase_order_detail.sql new file mode 100644 index 0000000..53601e6 --- /dev/null +++ b/db/query/purchase_order_detail.sql @@ -0,0 +1,12 @@ +-- name: CreatePurchaseOrderDetail :one +INSERT INTO purchase_order_detail ( + purchase_order_id, + merchant_id, + product_id, + quantity, + sub_total, + product_price +) VALUES ( + $1, $2, $3, $4, $5, $6 +) +RETURNING *; \ No newline at end of file diff --git a/db/query/suppliers.sql b/db/query/suppliers.sql new file mode 100644 index 0000000..83b0753 --- /dev/null +++ b/db/query/suppliers.sql @@ -0,0 +1,23 @@ +-- name: CreateSuppliers :one +INSERT INTO suppliers ( + merchant_id, + name, + detail +) VALUES ( + $1, $2, $3 +) +RETURNING *; + +-- name: SuppliersList :many +SELECT * FROM suppliers +WHERE merchant_id = $1 +ORDER BY index_id +LIMIT $2 +OFFSET $3; + +-- name: UpdateSupplier :one +SELECT * FROM suppliers +WHERE id = $1; + +-- name: DeleteSupplier :exec +DELETE FROM suppliers where id = $1; \ No newline at end of file diff --git a/db/sqlc/customers.sql.go b/db/sqlc/customers.sql.go new file mode 100644 index 0000000..b0a7ff3 --- /dev/null +++ b/db/sqlc/customers.sql.go @@ -0,0 +1,119 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: customers.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/tabbed/pqtype" +) + +const createCustomers = `-- name: CreateCustomers :one +INSERT INTO customers ( + merchant_id, + name, + detail +) VALUES ( + $1, $2, $3 +) +RETURNING id, index_id, merchant_id, name, detail, created_at, updated_at +` + +type CreateCustomersParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + Name string `json:"name"` + Detail pqtype.NullRawMessage `json:"detail"` +} + +func (q *Queries) CreateCustomers(ctx context.Context, arg CreateCustomersParams) (Customer, error) { + row := q.db.QueryRowContext(ctx, createCustomers, arg.MerchantID, arg.Name, arg.Detail) + var i Customer + err := row.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const customersList = `-- name: CustomersList :many +SELECT id, index_id, merchant_id, name, detail, created_at, updated_at FROM customers +WHERE merchant_id = $1 +ORDER BY index_id +LIMIT $2 +OFFSET $3 +` + +type CustomersListParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) CustomersList(ctx context.Context, arg CustomersListParams) ([]Customer, error) { + rows, err := q.db.QueryContext(ctx, customersList, arg.MerchantID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Customer + for rows.Next() { + var i Customer + if err := rows.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &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 deleteCustomer = `-- name: DeleteCustomer :exec +DELETE FROM customers where id = $1 +` + +func (q *Queries) DeleteCustomer(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteCustomer, id) + return err +} + +const updateCustomer = `-- name: UpdateCustomer :one +SELECT id, index_id, merchant_id, name, detail, created_at, updated_at FROM customers +WHERE id = $1 +` + +func (q *Queries) UpdateCustomer(ctx context.Context, id uuid.UUID) (Customer, error) { + row := q.db.QueryRowContext(ctx, updateCustomer, id) + var i Customer + err := row.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/db.go b/db/sqlc/db.go new file mode 100644 index 0000000..35e5f4a --- /dev/null +++ b/db/sqlc/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/db/sqlc/main_test.go b/db/sqlc/main_test.go new file mode 100644 index 0000000..7eb7f5c --- /dev/null +++ b/db/sqlc/main_test.go @@ -0,0 +1,30 @@ +package db + +import ( + "database/sql" + "log" + "os" + "testing" + + _ "github.com/lib/pq" +) + +const ( + dbDriver = "postgres" + dbSource = "postgresql://postgres:awksed123@localhost:5432/nice_pos?sslmode=disable" +) + +var testQueries *Queries +var testDB *sql.DB + +func TestMain(m *testing.M) { + var err error + testDB, err = sql.Open(dbDriver, dbSource) + if err != nil { + log.Fatal("cannot connect to db:", err) + } + + testQueries = New(testDB) + + os.Exit(m.Run()) +} diff --git a/db/sqlc/merchant.sql.go b/db/sqlc/merchant.sql.go new file mode 100644 index 0000000..f796c5f --- /dev/null +++ b/db/sqlc/merchant.sql.go @@ -0,0 +1,50 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: merchant.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const getMerchantById = `-- name: GetMerchantById :one +SELECT id, index_id, name, owner_id, created_at, updated_at FROM merchants +WHERE id = $1 +` + +func (q *Queries) GetMerchantById(ctx context.Context, id uuid.UUID) (Merchant, error) { + row := q.db.QueryRowContext(ctx, getMerchantById, id) + var i Merchant + err := row.Scan( + &i.ID, + &i.IndexID, + &i.Name, + &i.OwnerID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getMerchantByUserId = `-- name: GetMerchantByUserId :one +SELECT id, index_id, name, owner_id, created_at, updated_at FROM merchants +WHERE owner_id = $1 +` + +func (q *Queries) GetMerchantByUserId(ctx context.Context, ownerID uuid.UUID) (Merchant, error) { + row := q.db.QueryRowContext(ctx, getMerchantByUserId, ownerID) + var i Merchant + err := row.Scan( + &i.ID, + &i.IndexID, + &i.Name, + &i.OwnerID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/models.go b/db/sqlc/models.go new file mode 100644 index 0000000..9414dd1 --- /dev/null +++ b/db/sqlc/models.go @@ -0,0 +1,119 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package db + +import ( + "database/sql" + + "github.com/google/uuid" + "github.com/tabbed/pqtype" +) + +type Customer struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + MerchantID uuid.UUID `json:"merchant_id"` + Name string `json:"name"` + Detail pqtype.NullRawMessage `json:"detail"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type Merchant struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + Name string `json:"name"` + OwnerID uuid.UUID `json:"owner_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type Product struct { + ID uuid.UUID `json:"id"` + MerchantID uuid.UUID `json:"merchant_id"` + IndexID int64 `json:"index_id"` + Name string `json:"name"` + SellingPrice float64 `json:"selling_price"` + PurchasePrice float64 `json:"purchase_price"` + Stock float64 `json:"stock"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type PurchaseOrder struct { + ID uuid.UUID `json:"id"` + SupplierID uuid.UUID `json:"supplier_id"` + MerchantID uuid.UUID `json:"merchant_id"` + IndexID int64 `json:"index_id"` + Code sql.NullString `json:"code"` + IsPaid bool `json:"is_paid"` + Total float64 `json:"total"` + PaidNominal float64 `json:"paid_nominal"` + Note sql.NullString `json:"note"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type PurchaseOrderDetail struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + Code sql.NullString `json:"code"` + MerchantID uuid.UUID `json:"merchant_id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + ProductID uuid.UUID `json:"product_id"` + Quantity float64 `json:"quantity"` + SubTotal float64 `json:"sub_total"` + ProductPrice float64 `json:"product_price"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type SaleOrder struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + Code sql.NullString `json:"code"` + MerchantID uuid.UUID `json:"merchant_id"` + CustomerID uuid.NullUUID `json:"customer_id"` + IsPaid sql.NullBool `json:"is_paid"` + Total float64 `json:"total"` + PaidNominal float64 `json:"paid_nominal"` + Note sql.NullString `json:"note"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type SaleOrderDetail struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + SaleOrderID uuid.UUID `json:"sale_order_id"` + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + Quantity float64 `json:"quantity"` + SubTotal float64 `json:"sub_total"` + ProductPrice float64 `json:"product_price"` + Profit float64 `json:"profit"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type Supplier struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + MerchantID uuid.UUID `json:"merchant_id"` + Name string `json:"name"` + Detail pqtype.NullRawMessage `json:"detail"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type User struct { + ID uuid.UUID `json:"id"` + IndexID int64 `json:"index_id"` + Email string `json:"email"` + Password string `json:"password"` + Fullname sql.NullString `json:"fullname"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} diff --git a/db/sqlc/products.sql.go b/db/sqlc/products.sql.go new file mode 100644 index 0000000..e5d6f38 --- /dev/null +++ b/db/sqlc/products.sql.go @@ -0,0 +1,192 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: products.sql + +package db + +import ( + "context" + "database/sql" + + "github.com/google/uuid" +) + +const createProduct = `-- name: CreateProduct :one +INSERT INTO products ( + merchant_id, + name, + selling_price, + purchase_price, + stock +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING id, merchant_id, index_id, name, selling_price, purchase_price, stock, created_at, updated_at +` + +type CreateProductParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + Name string `json:"name"` + SellingPrice float64 `json:"selling_price"` + PurchasePrice float64 `json:"purchase_price"` + Stock float64 `json:"stock"` +} + +func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) (Product, error) { + row := q.db.QueryRowContext(ctx, createProduct, + arg.MerchantID, + arg.Name, + arg.SellingPrice, + arg.PurchasePrice, + arg.Stock, + ) + var i Product + err := row.Scan( + &i.ID, + &i.MerchantID, + &i.IndexID, + &i.Name, + &i.SellingPrice, + &i.PurchasePrice, + &i.Stock, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteProduct = `-- name: DeleteProduct :exec +DELETE FROM products WHERE id = $1 +` + +func (q *Queries) DeleteProduct(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteProduct, id) + return err +} + +const getProduct = `-- name: GetProduct :one +SELECT id, merchant_id, index_id, name, selling_price, purchase_price, stock, created_at, updated_at FROM products +WHERE id = $1 +` + +func (q *Queries) GetProduct(ctx context.Context, id uuid.UUID) (Product, error) { + row := q.db.QueryRowContext(ctx, getProduct, id) + var i Product + err := row.Scan( + &i.ID, + &i.MerchantID, + &i.IndexID, + &i.Name, + &i.SellingPrice, + &i.PurchasePrice, + &i.Stock, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listProducts = `-- name: ListProducts :many +SELECT id, merchant_id, index_id, name, selling_price, purchase_price, stock, created_at, updated_at FROM products +ORDER BY index_id +LIMIT $1 +OFFSET $2 +` + +type ListProductsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListProducts(ctx context.Context, arg ListProductsParams) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, listProducts, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Product + for rows.Next() { + var i Product + if err := rows.Scan( + &i.ID, + &i.MerchantID, + &i.IndexID, + &i.Name, + &i.SellingPrice, + &i.PurchasePrice, + &i.Stock, + &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 updateProduct = `-- name: UpdateProduct :one +UPDATE products +SET name = $2, selling_price = $3, purchase_price = $4, stock = $5, updated_at = $6 +WHERE id = $1 +RETURNING id, merchant_id, index_id, name, selling_price, purchase_price, stock, created_at, updated_at +` + +type UpdateProductParams struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SellingPrice float64 `json:"selling_price"` + PurchasePrice float64 `json:"purchase_price"` + Stock float64 `json:"stock"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) UpdateProduct(ctx context.Context, arg UpdateProductParams) (Product, error) { + row := q.db.QueryRowContext(ctx, updateProduct, + arg.ID, + arg.Name, + arg.SellingPrice, + arg.PurchasePrice, + arg.Stock, + arg.UpdatedAt, + ) + var i Product + err := row.Scan( + &i.ID, + &i.MerchantID, + &i.IndexID, + &i.Name, + &i.SellingPrice, + &i.PurchasePrice, + &i.Stock, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateProductStock = `-- name: UpdateProductStock :one +UPDATE products +SET stock = $1 +WHERE id = $2 +RETURNING stock +` + +type UpdateProductStockParams struct { + Stock float64 `json:"stock"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateProductStock(ctx context.Context, arg UpdateProductStockParams) (float64, error) { + row := q.db.QueryRowContext(ctx, updateProductStock, arg.Stock, arg.ID) + var stock float64 + err := row.Scan(&stock) + return stock, err +} diff --git a/db/sqlc/products_test.go b/db/sqlc/products_test.go new file mode 100644 index 0000000..bb16a11 --- /dev/null +++ b/db/sqlc/products_test.go @@ -0,0 +1,124 @@ +package db + +import ( + "context" + "database/sql" + "testing" + "time" + + "git.nochill.in/nochill/nice_pos/util" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func createRandomProduct(t *testing.T) (Product, CreateProductParams) { + + arg := CreateProductParams{ + MerchantID: uuid.MustParse("1f81d072-0c98-4e7e-9ceb-233d2eadb674"), + Name: util.RandomString(10), + SellingPrice: float64(123), + PurchasePrice: float64(123), + Stock: float64(120), + } + + product, err := testQueries.CreateProduct(context.Background(), arg) + require.NoError(t, err) + + return product, arg + +} + +func TestCreateProduct(t *testing.T) { + product, arg := createRandomProduct(t) + require.NotEmpty(t, product) + + require.Equal(t, arg.Name, product.Name) + require.Equal(t, arg.MerchantID, product.MerchantID) + require.Equal(t, arg.SellingPrice, product.SellingPrice) + require.Equal(t, arg.PurchasePrice, product.PurchasePrice) + require.Equal(t, arg.Stock, product.Stock) + + require.NotZero(t, product.ID) + require.NotZero(t, product.CreatedAt) + require.NotZero(t, product.UpdatedAt) +} + +func TestGetProduct(t *testing.T) { + createProduct, _ := createRandomProduct(t) + getProduct, err := testQueries.GetProduct(context.Background(), createProduct.ID) + + require.NoError(t, err) + require.NotEmpty(t, getProduct) + + require.Equal(t, createProduct.ID, getProduct.ID) + require.Equal(t, createProduct.MerchantID, getProduct.MerchantID) + require.Equal(t, createProduct.Name, getProduct.Name) + require.Equal(t, createProduct.PurchasePrice, getProduct.PurchasePrice) + require.Equal(t, createProduct.SellingPrice, getProduct.SellingPrice) + require.Equal(t, createProduct.Stock, getProduct.Stock) + + require.WithinDuration(t, createProduct.CreatedAt.Time, getProduct.CreatedAt.Time, time.Second) + require.WithinDuration(t, createProduct.UpdatedAt.Time, getProduct.UpdatedAt.Time, time.Second) +} + +func TestUpdateProduct(t *testing.T) { + createProduct, _ := createRandomProduct(t) + getProduct, err := testQueries.GetProduct(context.Background(), createProduct.ID) + + require.NoError(t, err) + + arg := UpdateProductParams{ + ID: getProduct.ID, + Name: util.RandomString(6), + SellingPrice: float64(200), + PurchasePrice: float64(200), + Stock: float64(200), + } + + updatedProduct, err := testQueries.UpdateProduct(context.Background(), arg) + + require.NotEmpty(t, updatedProduct) + require.NoError(t, err) + + require.Equal(t, arg.ID, updatedProduct.ID) + require.Equal(t, arg.Name, updatedProduct.Name) + require.Equal(t, arg.PurchasePrice, updatedProduct.PurchasePrice) + require.Equal(t, arg.SellingPrice, updatedProduct.SellingPrice) + require.Equal(t, arg.Stock, updatedProduct.Stock) + + require.NotSame(t, createProduct.Name, updatedProduct.Name) + require.NotSame(t, createProduct.SellingPrice, updatedProduct.SellingPrice) + require.NotSame(t, createProduct.PurchasePrice, updatedProduct.PurchasePrice) + require.NotSame(t, createProduct.Stock, updatedProduct.Stock) +} + +func TestDeleteProduct(t *testing.T) { + product1, _ := createRandomProduct(t) + err := testQueries.DeleteProduct(context.Background(), product1.ID) + require.NoError(t, err) + + product2, err := testQueries.GetProduct(context.Background(), product1.ID) + require.Error(t, err) + require.EqualError(t, err, sql.ErrNoRows.Error()) + require.Empty(t, product2) +} + +func TestGetProducts(t *testing.T) { + for i := 0; i < 6; i++ { + createRandomProduct(t) + } + + arg := ListProductsParams{ + Limit: 5, + Offset: 5, + } + + products, err := testQueries.ListProducts(context.Background(), arg) + require.NoError(t, err) + require.Len(t, products, 5) + + for _, product := range products { + require.NotEmpty(t, product) + } + +} diff --git a/db/sqlc/purchase_order.sql.go b/db/sqlc/purchase_order.sql.go new file mode 100644 index 0000000..c56de3e --- /dev/null +++ b/db/sqlc/purchase_order.sql.go @@ -0,0 +1,65 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: purchase_order.sql + +package db + +import ( + "context" + "database/sql" + + "github.com/google/uuid" +) + +const createPurchaseOrder = `-- name: CreatePurchaseOrder :one +INSERT INTO purchase_order ( + merchant_id, + supplier_id, + code, + is_paid, + total, + paid_nominal, + note +) VALUES ( + $1, $2, $3, $4, $5, $6, $7 +) +RETURNING id, supplier_id, merchant_id, index_id, code, is_paid, total, paid_nominal, note, created_at, updated_at +` + +type CreatePurchaseOrderParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + SupplierID uuid.UUID `json:"supplier_id"` + Code sql.NullString `json:"code"` + IsPaid bool `json:"is_paid"` + Total float64 `json:"total"` + PaidNominal float64 `json:"paid_nominal"` + Note sql.NullString `json:"note"` +} + +func (q *Queries) CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (PurchaseOrder, error) { + row := q.db.QueryRowContext(ctx, createPurchaseOrder, + arg.MerchantID, + arg.SupplierID, + arg.Code, + arg.IsPaid, + arg.Total, + arg.PaidNominal, + arg.Note, + ) + var i PurchaseOrder + err := row.Scan( + &i.ID, + &i.SupplierID, + &i.MerchantID, + &i.IndexID, + &i.Code, + &i.IsPaid, + &i.Total, + &i.PaidNominal, + &i.Note, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/purchase_order_detail.sql.go b/db/sqlc/purchase_order_detail.sql.go new file mode 100644 index 0000000..2dee965 --- /dev/null +++ b/db/sqlc/purchase_order_detail.sql.go @@ -0,0 +1,61 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: purchase_order_detail.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const createPurchaseOrderDetail = `-- name: CreatePurchaseOrderDetail :one +INSERT INTO purchase_order_detail ( + purchase_order_id, + merchant_id, + product_id, + quantity, + sub_total, + product_price +) VALUES ( + $1, $2, $3, $4, $5, $6 +) +RETURNING id, index_id, code, merchant_id, purchase_order_id, product_id, quantity, sub_total, product_price, created_at, updated_at +` + +type CreatePurchaseOrderDetailParams struct { + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + MerchantID uuid.UUID `json:"merchant_id"` + ProductID uuid.UUID `json:"product_id"` + Quantity float64 `json:"quantity"` + SubTotal float64 `json:"sub_total"` + ProductPrice float64 `json:"product_price"` +} + +func (q *Queries) CreatePurchaseOrderDetail(ctx context.Context, arg CreatePurchaseOrderDetailParams) (PurchaseOrderDetail, error) { + row := q.db.QueryRowContext(ctx, createPurchaseOrderDetail, + arg.PurchaseOrderID, + arg.MerchantID, + arg.ProductID, + arg.Quantity, + arg.SubTotal, + arg.ProductPrice, + ) + var i PurchaseOrderDetail + err := row.Scan( + &i.ID, + &i.IndexID, + &i.Code, + &i.MerchantID, + &i.PurchaseOrderID, + &i.ProductID, + &i.Quantity, + &i.SubTotal, + &i.ProductPrice, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/store.go b/db/sqlc/store.go new file mode 100644 index 0000000..c118f71 --- /dev/null +++ b/db/sqlc/store.go @@ -0,0 +1,88 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + "github.com/google/uuid" +) + +type Store struct { + *Queries + db *sql.DB +} + +func NewStore(db *sql.DB) *Store { + return &Store{ + db: db, + Queries: New(db), + } +} + +func (store *Store) execTx(ctx context.Context, fn func(*Queries) error) error { + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + q := New(tx) + err = fn(q) + if err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + return fmt.Errorf("tx err: %v, rb err : %v", err, rbErr) + } + return err + } + + return tx.Commit() + +} + +type PurchaseOrderProduct struct { + ProductID uuid.UUID `json:"product_id"` + Quantity float64 `json:"quantity"` + Sub_total float64 `json:"sub_total"` + Price float64 `json:"price"` +} + +type PurchasoOrderTxParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + SupplierID uuid.UUID `json:"supplier_id"` + Code sql.NullString `json:"code"` + IsPaid bool `json:"is_paid"` + Total float64 `json:"total"` + PaidNominal float64 `json:"paid_nominal"` + Note sql.NullString `json:"note"` + Products []PurchaseOrderProduct `json:"products"` +} + +type PurchaseOrderTxResult struct { + PurchaseOrder PurchaseOrder `json:"purchase_order"` + // PurchaseOrderDetail []PurchaseOrderDetail `json:"detail"` +} + +func (store *Store) PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxParams) (PurchaseOrderTxResult, error) { + var result PurchaseOrderTxResult + + err := store.execTx(ctx, func(q *Queries) error { + var err error + result.PurchaseOrder, err = q.CreatePurchaseOrder(ctx, CreatePurchaseOrderParams{ + MerchantID: arg.MerchantID, + SupplierID: arg.SupplierID, + Code: arg.Code, + IsPaid: arg.IsPaid, + Total: arg.Total, + PaidNominal: arg.PaidNominal, + Note: arg.Note, + }) + + if err != nil { + return err + } + + return nil + }) + + return result, err +} diff --git a/db/sqlc/store_test.go b/db/sqlc/store_test.go new file mode 100644 index 0000000..8aecab0 --- /dev/null +++ b/db/sqlc/store_test.go @@ -0,0 +1,45 @@ +package db + +import ( + "context" + "database/sql" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPurchaseOrder(t *testing.T) { + store := NewStore(testDB) + supplier, _ := createRandomSupplier(t) + product1, _ := createRandomProduct(t) + product2, _ := createRandomProduct(t) + + errs := make(chan error) + results := make(chan PurchaseOrderTxResult) + + // for i := 0; i < testIteration; i++ { + go func() { + result, err := store.PurchaseOrderTx(context.Background(), PurchasoOrderTxParams{ + MerchantID: supplier.MerchantID, + SupplierID: supplier.ID, + Code: sql.NullString{Valid: true, String: ""}, + IsPaid: true, + Total: product1.PurchasePrice + product2.PurchasePrice, + PaidNominal: product1.PurchasePrice + product2.PurchasePrice, + Note: sql.NullString{Valid: true, String: ""}, + // Products: products, + + }) + errs <- err + results <- result + }() + // } + + // for i := 0; i < testIteration; i++ { + err := <-errs + require.NoError(t, err) + + result := <-results + require.NotEmpty(t, result) + +} diff --git a/db/sqlc/suppliers.sql.go b/db/sqlc/suppliers.sql.go new file mode 100644 index 0000000..95805ee --- /dev/null +++ b/db/sqlc/suppliers.sql.go @@ -0,0 +1,119 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: suppliers.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/tabbed/pqtype" +) + +const createSuppliers = `-- name: CreateSuppliers :one +INSERT INTO suppliers ( + merchant_id, + name, + detail +) VALUES ( + $1, $2, $3 +) +RETURNING id, index_id, merchant_id, name, detail, created_at, updated_at +` + +type CreateSuppliersParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + Name string `json:"name"` + Detail pqtype.NullRawMessage `json:"detail"` +} + +func (q *Queries) CreateSuppliers(ctx context.Context, arg CreateSuppliersParams) (Supplier, error) { + row := q.db.QueryRowContext(ctx, createSuppliers, arg.MerchantID, arg.Name, arg.Detail) + var i Supplier + err := row.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteSupplier = `-- name: DeleteSupplier :exec +DELETE FROM suppliers where id = $1 +` + +func (q *Queries) DeleteSupplier(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteSupplier, id) + return err +} + +const suppliersList = `-- name: SuppliersList :many +SELECT id, index_id, merchant_id, name, detail, created_at, updated_at FROM suppliers +WHERE merchant_id = $1 +ORDER BY index_id +LIMIT $2 +OFFSET $3 +` + +type SuppliersListParams struct { + MerchantID uuid.UUID `json:"merchant_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) SuppliersList(ctx context.Context, arg SuppliersListParams) ([]Supplier, error) { + rows, err := q.db.QueryContext(ctx, suppliersList, arg.MerchantID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Supplier + for rows.Next() { + var i Supplier + if err := rows.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &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 updateSupplier = `-- name: UpdateSupplier :one +SELECT id, index_id, merchant_id, name, detail, created_at, updated_at FROM suppliers +WHERE id = $1 +` + +func (q *Queries) UpdateSupplier(ctx context.Context, id uuid.UUID) (Supplier, error) { + row := q.db.QueryRowContext(ctx, updateSupplier, id) + var i Supplier + err := row.Scan( + &i.ID, + &i.IndexID, + &i.MerchantID, + &i.Name, + &i.Detail, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/suppliers_test.go b/db/sqlc/suppliers_test.go new file mode 100644 index 0000000..99c9946 --- /dev/null +++ b/db/sqlc/suppliers_test.go @@ -0,0 +1,36 @@ +package db + +import ( + "context" + "testing" + + "git.nochill.in/nochill/nice_pos/util" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/tabbed/pqtype" +) + +func createRandomSupplier(t *testing.T) (Supplier, CreateSuppliersParams) { + arg := CreateSuppliersParams{ + MerchantID: uuid.MustParse("1f81d072-0c98-4e7e-9ceb-233d2eadb674"), + Name: util.RandomString(10), + Detail: pqtype.NullRawMessage{}, + } + + supplier, err := testQueries.CreateSuppliers(context.Background(), arg) + require.NoError(t, err) + + return supplier, arg + +} +func TestCreateSupplier(t *testing.T) { + supplier, arg := createRandomSupplier(t) + + require.Equal(t, arg.Name, supplier.Name) + require.Equal(t, arg.MerchantID, supplier.MerchantID) + + require.NotZero(t, supplier.ID) + require.NotZero(t, supplier.CreatedAt) + require.NotZero(t, supplier.UpdatedAt) + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7a97e1b --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.nochill.in/nochill/nice_pos + +go 1.20 + +require ( + github.com/google/uuid v1.3.0 + github.com/lib/pq v1.10.7 + github.com/stretchr/testify v1.8.2 + github.com/tabbed/pqtype v0.1.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..32cae5f --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tabbed/pqtype v0.1.1 h1:PhEcb9JZ8jr7SUjJDFjRPxny0M8fkXZrxn/a9yQfoZg= +github.com/tabbed/pqtype v0.1.1/go.mod h1:HLt2kLJPcUhODQkYn3mJkMHXVsuv3Z2n5NZEeKXL0Uk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..f24d0d2 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,11 @@ +version: "1" +packages: + - name: "db" + path: "./db/sqlc" + queries: "./db/query/" + schema: "./db/migrations/" + engine: "postgresql" + emit_json_tags: true + emit_prepared_queries: false + emit_interface: false + emit_exact_table_names: false diff --git a/util/random.go b/util/random.go new file mode 100644 index 0000000..260348d --- /dev/null +++ b/util/random.go @@ -0,0 +1,35 @@ +package util + +import ( + "math/rand" + "strings" + "time" +) + +const alphabet = "abcdefghijklmnopqrstuvwxyz" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// RandomInt generates a random integer between min and max +func RandomInt(min, max int64) int64 { + return min + rand.Int63n(max-min+1) +} + +func RandomFloat(max, div float64) float64 { + return (rand.Float64() * max) * div +} + +// RandomString generates a random string of length n +func RandomString(n int) string { + var sb strings.Builder + k := len(alphabet) + + for i := 0; i < n; i++ { + c := alphabet[rand.Intn(k)] + sb.WriteByte(c) + } + + return sb.String() +}