diff --git a/apps/web-app/app/pages/partner/[id]/invoice.vue b/apps/web-app/app/pages/partner/[id]/invoice.vue
new file mode 100644
index 00000000..fd1ed5dc
--- /dev/null
+++ b/apps/web-app/app/pages/partner/[id]/invoice.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/apps/web-app/app/pages/partner/[id]/kitchens.vue b/apps/web-app/app/pages/partner/[id]/kitchen.vue
similarity index 100%
rename from apps/web-app/app/pages/partner/[id]/kitchens.vue
rename to apps/web-app/app/pages/partner/[id]/kitchen.vue
diff --git a/apps/web-app/i18n/locales/ru-RU.json b/apps/web-app/i18n/locales/ru-RU.json
index 55f2b8c8..0186e2de 100644
--- a/apps/web-app/i18n/locales/ru-RU.json
+++ b/apps/web-app/i18n/locales/ru-RU.json
@@ -144,7 +144,9 @@
"agreements": "Договоры",
"prestige": "Престиж",
"activity-schedule": "Регламент активностей",
- "activity-schedules": "Регламенты активностей"
+ "activity-schedules": "Регламенты активностей",
+ "invoices": "Счета",
+ "invoice": "Счет"
},
"product": {
"available-for-purchase": "Доступен для покупки",
@@ -432,6 +434,9 @@
"epic-created": "Эпик создан",
"epic-updated": "Эпик обновлен",
"epic-deleted": "Эпик удален",
- "beacon-created": "Маяк создан"
+ "beacon-created": "Маяк создан",
+ "invoice-created": "Счет создан",
+ "invoice-updated": "Счет обновлен",
+ "invoice-deleted": "Счет удален"
}
}
diff --git a/apps/web-app/server/api/partner/id/[partnerId].patch.ts b/apps/web-app/server/api/partner/id/[partnerId]/index.patch.ts
similarity index 100%
rename from apps/web-app/server/api/partner/id/[partnerId].patch.ts
rename to apps/web-app/server/api/partner/id/[partnerId]/index.patch.ts
diff --git a/apps/web-app/server/api/partner/id/[partnerId]/invoice.post.ts b/apps/web-app/server/api/partner/id/[partnerId]/invoice.post.ts
new file mode 100644
index 00000000..e0821712
--- /dev/null
+++ b/apps/web-app/server/api/partner/id/[partnerId]/invoice.post.ts
@@ -0,0 +1,46 @@
+import type { Invoice } from '@roll-stack/database'
+import { createPartnerInvoiceSchema } from '#shared/services/partner'
+import { db } from '@roll-stack/database'
+import { type } from 'arktype'
+import { recountPartnerBalance } from '~~/server/services/invoice'
+
+export default defineEventHandler(async (event) => {
+ try {
+ const partnerId = getRouterParam(event, 'partnerId')
+ if (!partnerId) {
+ throw createError({
+ statusCode: 400,
+ message: 'Id is required',
+ })
+ }
+
+ const body = await readBody(event)
+ const data = createPartnerInvoiceSchema(body)
+ if (data instanceof type.errors) {
+ throw data
+ }
+
+ const partner = await db.partner.find(partnerId)
+ if (!partner) {
+ throw createError({
+ statusCode: 404,
+ message: 'Partner not found',
+ })
+ }
+
+ await db.invoice.create({
+ ...data,
+ type: data.type as Invoice['type'],
+ status: data.status as Invoice['status'],
+ partnerId: partner.id,
+ })
+
+ await recountPartnerBalance(partner.id)
+
+ return {
+ ok: true,
+ }
+ } catch (error) {
+ throw errorResolver(error)
+ }
+})
diff --git a/apps/web-app/server/api/partner/invoice/id/[invoiceId]/index.patch.ts b/apps/web-app/server/api/partner/invoice/id/[invoiceId]/index.patch.ts
new file mode 100644
index 00000000..11a076d7
--- /dev/null
+++ b/apps/web-app/server/api/partner/invoice/id/[invoiceId]/index.patch.ts
@@ -0,0 +1,54 @@
+import type { Invoice } from '@roll-stack/database'
+import { updatePartnerInvoiceSchema } from '#shared/services/partner'
+import { db } from '@roll-stack/database'
+import { type } from 'arktype'
+import { recountPartnerBalance } from '~~/server/services/invoice'
+
+export default defineEventHandler(async (event) => {
+ try {
+ const invoiceId = getRouterParam(event, 'invoiceId')
+ if (!invoiceId) {
+ throw createError({
+ statusCode: 400,
+ message: 'Id is required',
+ })
+ }
+
+ const body = await readBody(event)
+ const data = updatePartnerInvoiceSchema(body)
+ if (data instanceof type.errors) {
+ throw data
+ }
+
+ const invoice = await db.invoice.find(invoiceId)
+ if (!invoice) {
+ throw createError({
+ statusCode: 404,
+ message: 'Invoice not found',
+ })
+ }
+
+ await db.invoice.update(invoiceId, {
+ ...data,
+ type: data.type as Invoice['type'],
+ status: data.status as Invoice['status'],
+ })
+
+ // Recount partner balance
+ const partner = await db.partner.find(invoice.partnerId ?? '')
+ if (!partner) {
+ throw createError({
+ statusCode: 404,
+ message: 'Partner not found',
+ })
+ }
+
+ await recountPartnerBalance(partner.id)
+
+ return {
+ ok: true,
+ }
+ } catch (error) {
+ throw errorResolver(error)
+ }
+})
diff --git a/apps/web-app/server/services/invoice.ts b/apps/web-app/server/services/invoice.ts
new file mode 100644
index 00000000..d33df8ff
--- /dev/null
+++ b/apps/web-app/server/services/invoice.ts
@@ -0,0 +1,23 @@
+import { db } from '@roll-stack/database'
+
+export async function recountPartnerBalance(partnerId: string) {
+ const partnerInvoices = await db.invoice.listForPartner(partnerId)
+
+ let balance = 0
+ for (const invoice of partnerInvoices) {
+ if (invoice.type === 'replenishment' && invoice.status === 'paid') {
+ balance += invoice.total
+ }
+
+ if (invoice.type === 'royalties') {
+ balance -= invoice.total
+ }
+ if (invoice.type === 'other') {
+ balance -= invoice.total
+ }
+ }
+
+ await db.partner.update(partnerId, {
+ balance,
+ })
+}
diff --git a/apps/web-app/shared/services/partner.ts b/apps/web-app/shared/services/partner.ts
index 506e2992..2423550b 100644
--- a/apps/web-app/shared/services/partner.ts
+++ b/apps/web-app/shared/services/partner.ts
@@ -51,3 +51,21 @@ export const updatePartnerAgreementSchema = type({
patentStatus: patentStatus.describe('error.length.invalid').optional(),
})
export type UpdatePartnerAgreement = typeof updatePartnerAgreementSchema.infer
+
+export const createPartnerInvoiceSchema = type({
+ title: type('string').describe('error.length.invalid'),
+ description: type('string | undefined').describe('error.length.invalid').optional(),
+ total: type('number').describe('error.length.invalid'),
+ type: type('string').describe('error.length.invalid'),
+ status: type('string').describe('error.length.invalid'),
+})
+export type CreatePartnerInvoice = typeof createPartnerInvoiceSchema.infer
+
+export const updatePartnerInvoiceSchema = type({
+ title: type('string | undefined').describe('error.length.invalid').optional(),
+ description: type('string | undefined').describe('error.length.invalid').optional(),
+ total: type('number | undefined').describe('error.length.invalid').optional(),
+ type: type('string | undefined').describe('error.length.invalid').optional(),
+ status: type('string | undefined').describe('error.length.invalid').optional(),
+})
+export type UpdatePartnerInvoice = typeof updatePartnerInvoiceSchema.infer
diff --git a/packages/database/src/repository/index.ts b/packages/database/src/repository/index.ts
index 91cec7ab..8151ccf4 100644
--- a/packages/database/src/repository/index.ts
+++ b/packages/database/src/repository/index.ts
@@ -9,6 +9,7 @@ import { Epic } from './epic'
import { Feedback } from './feedback'
import { File } from './file'
import { Flow } from './flow'
+import { Invoice } from './invoice'
import { Kitchen } from './kitchen'
import { Locker } from './locker'
import { Media } from './media'
@@ -37,6 +38,7 @@ class Repository {
readonly feedback = Feedback
readonly file = File
readonly flow = Flow
+ readonly invoice = Invoice
readonly kitchen = Kitchen
readonly locker = Locker
readonly media = Media
diff --git a/packages/database/src/repository/invoice.ts b/packages/database/src/repository/invoice.ts
new file mode 100644
index 00000000..10410b4e
--- /dev/null
+++ b/packages/database/src/repository/invoice.ts
@@ -0,0 +1,39 @@
+import type { InvoiceDraft } from '../types'
+import { eq, sql } from 'drizzle-orm'
+import { useDatabase } from '../database'
+import { invoices } from '../tables'
+
+export class Invoice {
+ static async find(id: string) {
+ return useDatabase().query.invoices.findFirst({
+ where: (invoices, { eq }) => eq(invoices.id, id),
+ })
+ }
+
+ static async listForPartner(partnerId: string) {
+ return useDatabase().query.invoices.findMany({
+ where: (invoices, { eq }) => eq(invoices.partnerId, partnerId),
+ })
+ }
+
+ static async create(data: InvoiceDraft) {
+ const [invoice] = await useDatabase().insert(invoices).values(data).returning()
+ return invoice
+ }
+
+ static async update(id: string, data: Partial
) {
+ const [invoice] = await useDatabase()
+ .update(invoices)
+ .set({
+ ...data,
+ updatedAt: sql`now()`,
+ })
+ .where(eq(invoices.id, id))
+ .returning()
+ return invoice
+ }
+
+ static async delete(id: string) {
+ return useDatabase().delete(invoices).where(eq(invoices.id, id))
+ }
+}
diff --git a/packages/database/src/tables.ts b/packages/database/src/tables.ts
index 21177b05..838e05e6 100644
--- a/packages/database/src/tables.ts
+++ b/packages/database/src/tables.ts
@@ -788,8 +788,10 @@ export const invoices = pgTable('invoices', {
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
title: varchar('title').notNull(),
+ description: varchar('description'),
total: numeric('total', { mode: 'number' }).notNull().default(0),
paid: numeric('paid', { mode: 'number' }).notNull().default(0),
+ type: varchar('type').notNull().$type(),
status: varchar('status').notNull().$type().default('unpaid'),
partnerId: cuid2('partner_id').references(() => partners.id),
})
diff --git a/packages/database/src/types/entities.ts b/packages/database/src/types/entities.ts
index 027e5fce..d9bb43b2 100644
--- a/packages/database/src/types/entities.ts
+++ b/packages/database/src/types/entities.ts
@@ -97,4 +97,5 @@ export type ActivityScheduleTag = 'permanent'
| 'optional'
| 'advertising'
+export type InvoiceType = 'replenishment' | 'royalties' | 'other'
export type InvoiceStatus = 'paid' | 'unpaid'