billing invoice

This commit is contained in:
root
2026-05-25 05:35:55 -04:00
parent 8d0b7a5ceb
commit 33b6bb55f0
12 changed files with 5434 additions and 645 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+96 -8
View File
@@ -10,9 +10,11 @@ import {
companiesQuerySchema, rentersQuerySchema, auditLogQuerySchema, billingQuerySchema,
invoicesQuerySchema, adminCompanyUpdateSchema, companyStatusSchema,
createAdminSchema, adminRoleSchema, adminPermissionsSchema,
homepageUpdateSchema, idParamSchema, companyIdParamSchema, invoiceIdParamSchema,
homepageUpdateSchema, idParamSchema, companyIdParamSchema, billingAccountIdParamSchema, invoiceIdParamSchema,
pricingUpdateSchema, planFeatureCreateSchema, planFeatureUpdateSchema, planFeatureIdParamSchema,
promotionCreateSchema, promotionUpdateSchema, promotionIdParamSchema,
billingAccountUpdateSchema, createBillingInvoiceSchema, payBillingInvoiceSchema,
retryBillingInvoiceSchema, billingReasonSchema, billingCreditNoteSchema, billingRefundSchema,
} from './admin.schemas'
import { z } from 'zod'
@@ -203,13 +205,6 @@ router.get('/billing', requireAdminAuth, requireAdminRole('FINANCE'), async (req
} catch (err) { next(err) }
})
router.get('/billing/:companyId/invoices', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { companyId } = parseParams(companyIdParamSchema, req)
ok(res, await service.getCompanyInvoices(companyId, parseQuery(invoicesQuerySchema, req)))
} catch (err) { next(err) }
})
router.get('/billing/invoices/:invoiceId/pdf', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
@@ -221,6 +216,99 @@ router.get('/billing/invoices/:invoiceId/pdf', requireAdminAuth, requireAdminRol
} catch (err) { next(err) }
})
router.get('/billing/:companyId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { companyId } = parseParams(companyIdParamSchema, req)
ok(res, await service.getBillingAccountDetail(companyId))
} catch (err) { next(err) }
})
router.get('/billing/:companyId/invoices', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { companyId } = parseParams(companyIdParamSchema, req)
ok(res, await service.getCompanyInvoices(companyId, parseQuery(invoicesQuerySchema, req)))
} catch (err) { next(err) }
})
router.patch('/billing/accounts/:billingAccountId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { billingAccountId } = parseParams(billingAccountIdParamSchema, req)
ok(res, await service.updateBillingAccount(billingAccountId, parseBody(billingAccountUpdateSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/accounts/:billingAccountId/pause-dunning', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { billingAccountId } = parseParams(billingAccountIdParamSchema, req)
ok(res, await service.setDunningPaused(billingAccountId, true, req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/accounts/:billingAccountId/resume-dunning', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { billingAccountId } = parseParams(billingAccountIdParamSchema, req)
ok(res, await service.setDunningPaused(billingAccountId, false, req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/accounts/:billingAccountId/invoices', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { billingAccountId } = parseParams(billingAccountIdParamSchema, req)
created(res, await service.createBillingInvoice(billingAccountId, parseBody(createBillingInvoiceSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/finalize', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
ok(res, await service.finalizeBillingInvoice(invoiceId, req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/pay', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
ok(res, await service.payBillingInvoice(invoiceId, parseBody(payBillingInvoiceSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/retry-payment', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
ok(res, await service.retryBillingInvoicePayment(invoiceId, parseBody(retryBillingInvoiceSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/void', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
const { reason } = parseBody(billingReasonSchema, req)
ok(res, await service.voidBillingInvoice(invoiceId, reason, req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/uncollectible', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
const { reason } = parseBody(billingReasonSchema, req)
ok(res, await service.markBillingInvoiceUncollectible(invoiceId, reason, req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/credit-notes', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
ok(res, await service.issueBillingCreditNote(invoiceId, parseBody(billingCreditNoteSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
router.post('/billing/invoices/:invoiceId/refunds', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
try {
const { invoiceId } = parseParams(invoiceIdParamSchema, req)
ok(res, await service.issueBillingRefund(invoiceId, parseBody(billingRefundSchema, req), req.admin.id, req.ip))
} catch (err) { next(err) }
})
// ─── Pricing config ────────────────────────────────────────────
router.get('/pricing', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
@@ -45,6 +45,7 @@ export const auditLogQuerySchema = z.object({
})
export const billingQuerySchema = z.object({
q: z.string().optional(),
status: z.string().optional(),
plan: z.string().optional(),
page: z.coerce.number().int().min(1).default(1),
@@ -167,10 +168,91 @@ export const companyIdParamSchema = z.object({
companyId: z.string().min(1),
})
export const billingAccountIdParamSchema = z.object({
billingAccountId: z.string().min(1),
})
export const invoiceIdParamSchema = z.object({
invoiceId: z.string().min(1),
})
export const billingAccountUpdateSchema = z.object({
legalName: z.string().min(1).max(255).optional(),
billingEmail: z.string().email().optional(),
billingAddress: z.any().optional(),
taxId: z.union([z.string().max(120), z.null()]).optional(),
taxExempt: z.boolean().optional(),
invoiceTerms: z.enum(['DUE_ON_RECEIPT', 'NET_7', 'NET_15', 'NET_30', 'NET_45', 'NET_60']).optional(),
netTermsDays: z.number().int().min(0).max(365).optional(),
})
export const billingLineItemInputSchema = z.object({
type: z.enum([
'SUBSCRIPTION_FEE',
'SEAT_FEE',
'USAGE_FEE',
'SETUP_FEE',
'DISCOUNT',
'TAX',
'CREDIT',
'PRORATION',
'REFUND_ADJUSTMENT',
'MANUAL_ADJUSTMENT',
'PROFESSIONAL_SERVICES',
'CANCELLATION_FEE',
]),
description: z.string().min(1).max(255),
quantity: z.number().int().positive().max(100000),
unitAmount: z.number().int(),
periodStart: nullableDate,
periodEnd: nullableDate,
})
export const createBillingInvoiceSchema = z.object({
subscriptionId: z.union([z.string(), z.null()]).optional(),
invoiceType: z.enum([
'SUBSCRIPTION_INITIAL',
'SUBSCRIPTION_RENEWAL',
'TRIAL_CONVERSION',
'SUBSCRIPTION_UPGRADE',
'SUBSCRIPTION_DOWNGRADE_CREDIT',
'USAGE_BASED',
'ONE_TIME',
'MANUAL',
'CORRECTION',
'CANCELLATION_FEE',
]),
currency: z.string().min(3).max(8).optional(),
dueAt: nullableDate,
isSubscriptionBlocking: z.boolean().optional(),
adminReason: z.union([z.string().max(500), z.null()]).optional(),
lineItems: z.array(billingLineItemInputSchema).min(1),
})
export const payBillingInvoiceSchema = z.object({
amount: z.number().int().positive().optional(),
paymentMethodId: z.union([z.string(), z.null()]).optional(),
providerPaymentId: z.union([z.string(), z.null()]).optional(),
})
export const retryBillingInvoiceSchema = z.object({
paymentMethodId: z.union([z.string(), z.null()]).optional(),
})
export const billingReasonSchema = z.object({
reason: z.string().min(1).max(500),
})
export const billingCreditNoteSchema = z.object({
amount: z.number().int().positive(),
reason: z.string().min(1).max(500),
})
export const billingRefundSchema = z.object({
amount: z.number().int().positive(),
reason: z.string().min(1).max(500),
})
export const homepageUpdateSchema = z.object({
homepage: marketplaceHomepageConfigSchema,
})
+83 -51
View File
@@ -4,19 +4,13 @@ import crypto from 'crypto'
import { authenticator } from 'otplib'
import qrcode from 'qrcode'
import { getMarketplaceHomepageContent, saveMarketplaceHomepageContent } from '../../services/platformContentService'
import { generateInvoicePdf, buildInvoiceNumber } from '../../services/invoicePdfService'
import { sendTransactionalEmail } from '../../services/notificationService'
import * as presenter from './admin.presenter'
import * as repo from './admin.repo'
import * as billingService from './admin.billing.service'
const ADMIN_RESET_TTL_MINUTES = 60
const PLAN_MONTHLY_AMOUNT: Record<string, number> = {
STARTER: 2990,
GROWTH: 3990,
PRO: 99900,
}
function signAdminToken(adminId: string) {
return jwt.sign({ sub: adminId, type: 'admin' }, process.env.JWT_SECRET!, { expiresIn: '8h' })
}
@@ -226,58 +220,96 @@ export async function updateAdminPermissions(id: string, permissions: any[]) {
return presenter.presentAdminUser(admin)
}
export async function getBilling(query: { status?: string; plan?: string; page: number; pageSize: number }) {
const [{ data, total }, activeSubscriptions, stats] = await Promise.all([
repo.listBillingPage(query),
repo.listActiveSubscriptionsForMrr(),
repo.getBillingStatusCounts(),
])
const mrr = activeSubscriptions.reduce((acc, subscription) => {
const monthlyAmount = PLAN_MONTHLY_AMOUNT[subscription.plan] ?? 0
return acc + (subscription.billingPeriod === 'ANNUAL' ? Math.round(monthlyAmount * 10 / 12) : monthlyAmount)
}, 0)
return presenter.presentPaginated(data, total, query.page, query.pageSize, {
stats: { mrr, ...stats },
})
export async function getBilling(query: { q?: string; status?: string; plan?: string; page: number; pageSize: number }) {
const { data, total, stats } = await billingService.listBillingAccounts(query)
return presenter.presentPaginated(data, total, query.page, query.pageSize, { stats })
}
export async function getCompanyInvoices(companyId: string, query: { page: number; pageSize: number }) {
const { data, total } = await repo.listCompanyInvoicesPage(companyId, query)
const detail = await billingService.getBillingAccountDetail(companyId)
const start = (query.page - 1) * query.pageSize
const end = start + query.pageSize
const data = detail.invoices.slice(start, end)
const total = detail.invoices.length
return presenter.presentPaginated(data, total, query.page, query.pageSize)
}
export async function getInvoicePdf(invoiceId: string) {
const invoice = await repo.getInvoicePdfRecord(invoiceId)
const invoiceNumber = buildInvoiceNumber(invoice.id, invoice.createdAt)
const transactionId = invoice.amanpayTransactionId ?? invoice.paypalCaptureId ?? null
const pdfBuffer = await generateInvoicePdf({
invoiceNumber,
issueDate: invoice.createdAt.toISOString(),
dueDate: invoice.status === 'PENDING' ? invoice.createdAt.toISOString() : null,
company: {
name: invoice.company.name,
email: invoice.company.email,
phone: invoice.company.phone,
address: invoice.company.address,
},
subscription: {
plan: invoice.subscription.plan,
billingPeriod: invoice.subscription.billingPeriod,
currentPeriodStart: invoice.subscription.currentPeriodStart?.toISOString(),
currentPeriodEnd: invoice.subscription.currentPeriodEnd?.toISOString(),
currency: invoice.subscription.currency,
},
amount: invoice.amount,
currency: invoice.currency,
status: invoice.status,
paymentProvider: invoice.paymentProvider,
transactionId,
paidAt: invoice.paidAt?.toISOString(),
})
return billingService.getInvoicePdf(invoiceId)
}
return { pdfBuffer, invoiceNumber }
export function getBillingAccountDetail(companyId: string) {
return billingService.getBillingAccountDetail(companyId)
}
export function updateBillingAccount(
billingAccountId: string,
data: Parameters<typeof billingService.updateBillingAccount>[1],
adminId: string,
ip?: string,
) {
return billingService.updateBillingAccount(billingAccountId, data, adminId, ip)
}
export function setDunningPaused(billingAccountId: string, paused: boolean, adminId: string, ip?: string) {
return billingService.setDunningPaused(billingAccountId, paused, adminId, ip)
}
export function createBillingInvoice(
billingAccountId: string,
data: Parameters<typeof billingService.createDraftInvoice>[1],
adminId: string,
ip?: string,
) {
return billingService.createDraftInvoice(billingAccountId, data, adminId, ip)
}
export function finalizeBillingInvoice(invoiceId: string, adminId: string, ip?: string) {
return billingService.finalizeInvoice(invoiceId, adminId, ip)
}
export function payBillingInvoice(
invoiceId: string,
data: Parameters<typeof billingService.payInvoice>[1],
adminId: string,
ip?: string,
) {
return billingService.payInvoice(invoiceId, data, adminId, ip)
}
export function retryBillingInvoicePayment(
invoiceId: string,
data: Parameters<typeof billingService.retryInvoicePayment>[1],
adminId: string,
ip?: string,
) {
return billingService.retryInvoicePayment(invoiceId, data, adminId, ip)
}
export function voidBillingInvoice(invoiceId: string, reason: string, adminId: string, ip?: string) {
return billingService.voidInvoice(invoiceId, reason, adminId, ip)
}
export function markBillingInvoiceUncollectible(invoiceId: string, reason: string, adminId: string, ip?: string) {
return billingService.markInvoiceUncollectible(invoiceId, reason, adminId, ip)
}
export function issueBillingCreditNote(
invoiceId: string,
data: Parameters<typeof billingService.issueCreditNote>[1],
adminId: string,
ip?: string,
) {
return billingService.issueCreditNote(invoiceId, data, adminId, ip)
}
export function issueBillingRefund(
invoiceId: string,
data: Parameters<typeof billingService.issueRefund>[1],
adminId: string,
ip?: string,
) {
return billingService.issueRefund(invoiceId, data, adminId, ip)
}
export function getMarketplaceHomepage() {
+88 -15
View File
@@ -11,7 +11,7 @@ interface InvoiceData {
phone?: string | null
address?: any
}
subscription: {
subscription?: {
plan: string
billingPeriod: string
currentPeriodStart?: string | null
@@ -24,6 +24,24 @@ interface InvoiceData {
paymentProvider: string
transactionId?: string | null
paidAt?: string | null
lineItems?: Array<{
description: string
amount: number
currency: string
quantity?: number
unitAmount?: number
periodStart?: string | null
periodEnd?: string | null
}>
totals?: {
subtotalAmount: number
discountAmount: number
creditAmount: number
taxAmount: number
totalAmount: number
amountPaid: number
amountDue: number
}
}
const PLAN_LABEL: Record<string, string> = {
@@ -40,8 +58,15 @@ const PERIOD_LABEL: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
PAID: '#10b981',
PENDING: '#f59e0b',
OPEN: '#f59e0b',
PAYMENT_PENDING: '#f59e0b',
PARTIALLY_PAID: '#f59e0b',
PAST_DUE: '#ef4444',
FAILED: '#ef4444',
REFUNDED: '#6b7280',
PARTIALLY_REFUNDED: '#6b7280',
VOID: '#6b7280',
UNCOLLECTIBLE: '#6b7280',
}
const colors = {
@@ -307,12 +332,26 @@ function formatAddress(address: any): string {
function InvoiceDocument({ data }: { data: InvoiceData }) {
const statusColor = STATUS_COLORS[data.status] ?? '#6b7280'
const addressStr = formatAddress(data.company.address)
const planLabel = PLAN_LABEL[data.subscription.plan] ?? data.subscription.plan
const periodLabel = PERIOD_LABEL[data.subscription.billingPeriod] ?? data.subscription.billingPeriod
const description = `${planLabel} Plan — ${periodLabel} Subscription`
const periodStr = data.subscription.currentPeriodStart && data.subscription.currentPeriodEnd
? `${fmtDate(data.subscription.currentPeriodStart)} ${fmtDate(data.subscription.currentPeriodEnd)}`
: '—'
const planLabel = data.subscription ? (PLAN_LABEL[data.subscription.plan] ?? data.subscription.plan) : 'Manual'
const periodLabel = data.subscription ? (PERIOD_LABEL[data.subscription.billingPeriod] ?? data.subscription.billingPeriod) : 'Custom'
const lineItems = data.lineItems?.length
? data.lineItems
: [{
description: `${planLabel} Plan — ${periodLabel} Subscription`,
amount: data.amount,
currency: data.currency,
periodStart: data.subscription?.currentPeriodStart ?? null,
periodEnd: data.subscription?.currentPeriodEnd ?? null,
}]
const totals = data.totals ?? {
subtotalAmount: data.amount,
discountAmount: 0,
creditAmount: 0,
taxAmount: 0,
totalAmount: data.amount,
amountPaid: data.paidAt ? data.amount : 0,
amountDue: data.paidAt ? 0 : data.amount,
}
return React.createElement(
Document,
@@ -397,12 +436,14 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
React.createElement(Text, { style: s.metaKey }, 'Billing Period'),
React.createElement(Text, { style: s.metaValue }, periodLabel),
),
React.createElement(
data.subscription
? React.createElement(
View,
{ style: s.metaRow },
React.createElement(Text, { style: s.metaKey }, 'Plan'),
React.createElement(Text, { style: s.metaValue }, planLabel),
),
)
: null,
),
),
@@ -417,13 +458,21 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
React.createElement(Text, { style: [s.tableHeaderCell, s.col20Center] }, 'Period'),
React.createElement(Text, { style: [s.tableHeaderCell, s.col20] }, 'Amount'),
),
React.createElement(
...lineItems.map((item) => {
const periodStr = item.periodStart && item.periodEnd
? `${fmtDate(item.periodStart)} ${fmtDate(item.periodEnd)}`
: '—'
const description = item.quantity && item.quantity > 1 && item.unitAmount !== undefined
? `${item.description} (${item.quantity} × ${fmt(item.unitAmount, item.currency)})`
: item.description
return React.createElement(
View,
{ style: s.tableRow },
{ key: `${item.description}-${item.amount}-${item.periodStart ?? ''}`, style: s.tableRow },
React.createElement(Text, { style: [s.tableCell, s.col60] }, description),
React.createElement(Text, { style: [s.tableCellMuted, s.col20Center, { textAlign: 'center' }] }, periodStr),
React.createElement(Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, fmt(data.amount, data.currency)),
),
React.createElement(Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, fmt(item.amount, item.currency)),
)
}),
),
// ── Totals ───────────────────────────────────────────────────
@@ -431,13 +480,37 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Subtotal'),
React.createElement(Text, { style: s.totalValue }, fmt(data.amount, data.currency)),
React.createElement(Text, { style: s.totalValue }, fmt(totals.subtotalAmount, data.currency)),
),
totals.discountAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Discounts'),
React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.discountAmount, data.currency)}`),
)
: null,
totals.creditAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Credits'),
React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.creditAmount, data.currency)}`),
)
: null,
totals.taxAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Tax'),
React.createElement(Text, { style: s.totalValue }, fmt(totals.taxAmount, data.currency)),
)
: null,
React.createElement(
View,
{ style: s.grandTotalRow },
React.createElement(Text, { style: s.grandTotalLabel }, 'Total Due'),
React.createElement(Text, { style: s.grandTotalValue }, fmt(data.amount, data.currency)),
React.createElement(Text, { style: s.grandTotalValue }, fmt(totals.totalAmount, data.currency)),
),
// ── Payment Info ─────────────────────────────────────────────
@@ -136,4 +136,194 @@ describe('Admin API', () => {
)
})
})
describe('Billing admin workflows', () => {
let billingAccountId: string
beforeAll(async () => {
const res = await request(app)
.get('/api/v1/admin/billing')
.set(authHeader(financeToken))
expect(res.status).toBe(200)
const account = res.body.data.data.find((item: any) => item.company.id === companyId)
expect(account).toBeTruthy()
billingAccountId = account.id
})
it('returns billing account detail', async () => {
const res = await request(app)
.get(`/api/v1/admin/billing/${companyId}`)
.set(authHeader(financeToken))
expect(res.status).toBe(200)
expect(res.body.data.id).toBe(billingAccountId)
expect(res.body.data.company.id).toBe(companyId)
expect(Array.isArray(res.body.data.invoices)).toBe(true)
})
it('supports create, finalize, pay, refund, and pdf download', async () => {
const draftRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'MANUAL',
adminReason: 'Enterprise onboarding',
lineItems: [
{
type: 'SETUP_FEE',
description: 'Onboarding setup',
quantity: 1,
unitAmount: 50000,
},
{
type: 'PROFESSIONAL_SERVICES',
description: 'Migration assistance',
quantity: 2,
unitAmount: 15000,
},
],
})
expect(draftRes.status).toBe(201)
expect(draftRes.body.data.status).toBe('DRAFT')
const invoiceId = draftRes.body.data.id as string
const finalizeRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeRes.status).toBe(200)
expect(finalizeRes.body.data.status).toBe('OPEN')
expect(finalizeRes.body.data.invoiceNumber).toMatch(/^INV-\d{4}-\d{6}$/)
expect(finalizeRes.body.data.totalAmount).toBe(80000)
const pdfRes = await request(app)
.get(`/api/v1/admin/billing/invoices/${invoiceId}/pdf`)
.set(authHeader(financeToken))
expect(pdfRes.status).toBe(200)
expect(pdfRes.headers['content-type']).toContain('application/pdf')
const payRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/pay`)
.set(authHeader(financeToken))
.send({ amount: 80000, providerPaymentId: 'manual-payment-1' })
expect(payRes.status).toBe(200)
expect(payRes.body.data.status).toBe('PAID')
expect(payRes.body.data.amountDue).toBe(0)
const refundRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/refunds`)
.set(authHeader(financeToken))
.send({ amount: 20000, reason: 'Service credit after go-live issue' })
expect(refundRes.status).toBe(200)
expect(refundRes.body.data.status).toBe('PARTIALLY_REFUNDED')
})
it('supports credit notes, dunning controls, voids, and write-offs', async () => {
const pauseRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/pause-dunning`)
.set(authHeader(financeToken))
.send({})
expect(pauseRes.status).toBe(200)
expect(pauseRes.body.data.dunningPaused).toBe(true)
const invoiceRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'MANUAL',
lineItems: [
{
type: 'MANUAL_ADJUSTMENT',
description: 'Support package',
quantity: 1,
unitAmount: 30000,
},
],
})
expect(invoiceRes.status).toBe(201)
const invoiceId = invoiceRes.body.data.id as string
const finalizeRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeRes.status).toBe(200)
expect(finalizeRes.body.data.status).toBe('OPEN')
const creditRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/credit-notes`)
.set(authHeader(financeToken))
.send({ amount: 5000, reason: 'Manual goodwill credit' })
expect(creditRes.status).toBe(200)
expect(creditRes.body.data.amountDue).toBe(25000)
const retryRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/retry-payment`)
.set(authHeader(financeToken))
.send({})
expect(retryRes.status).toBe(200)
expect(retryRes.body.data.status).toBe('PAYMENT_PENDING')
const voidRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${invoiceId}/void`)
.set(authHeader(financeToken))
.send({ reason: 'Customer rolled into replacement invoice' })
expect(voidRes.status).toBe(200)
expect(voidRes.body.data.status).toBe('VOID')
const badDebtRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/invoices`)
.set(authHeader(financeToken))
.send({
invoiceType: 'SUBSCRIPTION_RENEWAL',
isSubscriptionBlocking: true,
lineItems: [
{
type: 'SUBSCRIPTION_FEE',
description: 'Renewal period',
quantity: 1,
unitAmount: 12000,
},
],
})
expect(badDebtRes.status).toBe(201)
const badDebtInvoiceId = badDebtRes.body.data.id as string
const finalizeBadDebtRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${badDebtInvoiceId}/finalize`)
.set(authHeader(financeToken))
.send({})
expect(finalizeBadDebtRes.status).toBe(200)
const writeoffRes = await request(app)
.post(`/api/v1/admin/billing/invoices/${badDebtInvoiceId}/uncollectible`)
.set(authHeader(financeToken))
.send({ reason: 'Customer account closed after failed recovery' })
expect(writeoffRes.status).toBe(200)
expect(writeoffRes.body.data.status).toBe('UNCOLLECTIBLE')
const resumeRes = await request(app)
.post(`/api/v1/admin/billing/accounts/${billingAccountId}/resume-dunning`)
.set(authHeader(financeToken))
.send({})
expect(resumeRes.status).toBe(200)
expect(resumeRes.body.data.dunningPaused).toBe(false)
})
})
})
+11
View File
@@ -2,6 +2,17 @@ import { beforeAll, afterAll } from 'vitest'
import { prisma } from '../lib/prisma'
const delegates = [
'billingEvent',
'billingRefund',
'billingCreditNote',
'billingCreditLedgerEntry',
'billingCreditBalance',
'billingPaymentAttempt',
'billingPaymentIntent',
'billingInvoiceLineItem',
'billingInvoice',
'billingPaymentMethod',
'billingAccount',
'auditLog',
'adminPermission',
'adminUser',
+634
View File
@@ -0,0 +1,634 @@
# Contrat de Location de Véhicule
**Contrat n° :** [●]
**Date de signature :** [●]
**Lieu de signature :** [Ville, Maroc]
---
## 1. Parties au contrat
Entre les soussignés :
### Le Loueur / Agence
**Nom commercial :** [●]
**Raison sociale :** [●]
**Forme juridique :** [●]
**RC n° :** [●]
**ICE n° :** [●]
**IF n° :** [●]
**Agrément / Autorisation dexercice n° :** [●]
**Date de délivrance de lagrément :** [●]
**Autorité délivrante :** [●]
**Adresse :** [●]
**Téléphone :** [●]
**Email :** [●]
**Représenté par :** [Nom, prénom, fonction]
Ci-après désigné **« le Loueur »**,
Et :
### Le Locataire / Conducteur principal
**Nom :** [●]
**Prénom :** [●]
**Date de naissance :** [●]
**Nationalité :** [●]
**CIN / Passeport n° :** [●]
**Adresse complète :** [●]
**Téléphone :** [●]
**Email :** [●]
Ci-après désigné **« le Locataire »**.
Le Loueur et le Locataire sont ensemble désignés **« les Parties »**.
---
## 2. Responsable légal et conformité réglementaire du Loueur
Le Loueur déclare exercer lactivité de location de véhicules sans chauffeur conformément à la réglementation marocaine applicable.
La personne responsable de lactivité est :
**Nom et prénom :** [●]
**Qualité :** [Dirigeant / Actionnaire / Salarié]
**Pièce didentité n° :** [●]
**Qualification / diplôme / expérience :** [●]
**Téléphone :** [●]
**Email :** [●]
Cette personne est responsable du suivi des contrats, de lentretien des véhicules, de leur conformité aux normes de sécurité routière et du respect des obligations administratives applicables.
Le Loueur déclare notamment :
- disposer des autorisations administratives requises pour lexercice de lactivité ;
- être régulièrement immatriculé auprès des administrations compétentes ;
- disposer dun siège social ou local professionnel déclaré ;
- respecter les obligations sociales, fiscales et administratives applicables ;
- exploiter une flotte conforme aux seuils et conditions réglementaires applicables ;
- maintenir les véhicules loués en état conforme aux normes de sécurité routière.
---
## 3. Permis de conduire
Le Locataire déclare être titulaire dun permis de conduire valide :
**Numéro du permis :** [●]
**Catégorie :** [B / autre]
**Date de délivrance :** [●]
**Date dexpiration :** [●]
**Pays de délivrance :** [●]
**Permis international, le cas échéant :** [Oui / Non, n° ●]
Le Locataire garantit que son permis est valide pendant toute la durée de location et quil nest frappé daucune suspension, annulation ou restriction incompatible avec la conduite du véhicule loué.
---
## 4. Conducteur(s) autorisé(s)
Le véhicule ne peut être conduit que par le Locataire et, le cas échéant, par les conducteurs additionnels expressément mentionnés ci-dessous :
### Conducteur additionnel 1
**Nom et prénom :** [●]
**CIN / Passeport :** [●]
**Permis n° :** [●]
**Catégorie :** [●]
**Date de délivrance :** [●]
**Téléphone :** [●]
Tout conducteur non déclaré est interdit. En cas daccident, vol, infraction ou dommage causé par un conducteur non autorisé, le Locataire assume lentière responsabilité des conséquences financières, civiles, pénales et assurantielles.
---
## 5. Véhicule loué
Le Loueur met à disposition du Locataire le véhicule suivant :
**Marque :** [●]
**Modèle :** [●]
**Année :** [●]
**Couleur :** [●]
**Immatriculation :** [●]
**Numéro de châssis :** [●]
**Propriétaire du véhicule :** [Loueur / Autre agence / Société de financement / Autre]
**Justificatif dexploitation si le véhicule appartient à un tiers :** [Contrat / Autorisation / Mandat / Autre]
**Type de motorisation :** [Thermique / Hybride / Électrique]
**Date de première mise en circulation :** [●]
**Âge du véhicule à la date de location :** [●]
**Véhicule conforme aux limites réglementaires dexploitation :** [Oui / Non]
**Kilométrage au départ :** [●] km
**Niveau de carburant au départ :** [●]
**Carte grise :** [Oui / Non]
**Assurance :** [Oui / Non]
**Visite technique :** [Oui / Non / Non applicable]
Un état des lieux contradictoire au départ et au retour est annexé au présent contrat et en fait partie intégrante.
Le Loueur déclare que le véhicule mis à disposition est conforme aux conditions réglementaires dexploitation applicables à sa catégorie, notamment en matière d’âge, dimmatriculation, dassurance, dentretien et de sécurité routière.
---
## 6. Entretien et conformité du véhicule
Le Loueur déclare que le véhicule est régulièrement entretenu, assuré, immatriculé et conforme aux normes de sécurité routière applicables.
Le Locataire reconnaît avoir reçu un véhicule en état apparent de fonctionnement, sous réserve des observations mentionnées dans l’état des lieux de départ.
En cas danomalie mécanique, voyant dalerte, bruit anormal, crevaison, panne ou incident susceptible daffecter la sécurité, le Locataire doit cesser lutilisation du véhicule dès que les conditions de sécurité le permettent et informer immédiatement le Loueur.
---
## 7. Durée de location
La location commence le :
**Date :** [●]
**Heure :** [●]
**Lieu de prise en charge :** [●]
La location prend fin le :
**Date :** [●]
**Heure :** [●]
**Lieu de restitution :** [●]
Toute prolongation doit être acceptée préalablement par écrit par le Loueur. À défaut, le véhicule sera considéré comme conservé sans autorisation, et le Loueur pourra facturer les jours supplémentaires, pénalités de retard et frais éventuels.
---
## 8. Prix de location et modalités de paiement
Le prix de location est fixé comme suit :
| Désignation | Montant |
|---|---:|
| Tarif journalier HT | [●] MAD |
| Nombre de jours | [●] |
| Sous-total HT | [●] MAD |
| TVA applicable | [●] % |
| Montant TVA | [●] MAD |
| Total TTC | [●] MAD |
| Conducteur additionnel | [●] MAD |
| Livraison / récupération | [●] MAD |
| Siège enfant | [●] MAD |
| GPS / accessoires | [●] MAD |
| Autres frais | [●] MAD |
**Montant total à payer TTC : [●] MAD**
**Mode de paiement :** [Espèces / carte bancaire / virement / chèque / autre]
**Date de paiement :** [●]
**Référence de paiement :** [●]
Le Locataire reconnaît avoir reçu une information claire sur le prix total TTC avant la signature du contrat.
---
## 9. Dépôt de garantie / caution
Le Locataire verse au Loueur un dépôt de garantie de :
**Montant : [●] MAD**
**Forme de la caution :** [Préautorisation bancaire / espèces / chèque / autre]
**Date de constitution :** [●]
**Conditions de libération :** [●]
Le dépôt de garantie garantit notamment :
- les dommages non couverts par lassurance ;
- la franchise dassurance ;
- les kilomètres supplémentaires ;
- le carburant manquant ;
- les frais de nettoyage exceptionnel ;
- les contraventions, amendes, péages ou frais administratifs ;
- les retards de restitution ;
- la perte de documents, clés, accessoires ou équipements.
La caution sera restituée après vérification de l’état du véhicule, du kilométrage, du carburant, des éventuelles infractions et des sommes restant dues.
---
## 10. Assurance
Le véhicule est assuré auprès de :
**Compagnie dassurance :** [●]
**Numéro de police :** [●]
**Type de couverture :** [Responsabilité civile / Tous risques / autre]
**Franchise applicable :** [●] MAD
**Assistance :** [Oui / Non]
**Numéro dassistance :** [●]
Le Locataire reconnaît avoir été informé des garanties, exclusions et franchises applicables.
Sont notamment exclus de la couverture, sauf stipulation contraire de lassureur :
- conduite par une personne non autorisée ;
- conduite sans permis valide ;
- conduite sous lemprise dalcool, stupéfiants ou substances interdites ;
- usage du véhicule hors des voies autorisées ;
- participation à des courses, essais ou compétitions ;
- négligence grave ;
- fausse déclaration ;
- absence de déclaration daccident dans les délais ;
- vol avec clés laissées dans le véhicule ;
- transport illicite de personnes ou de marchandises.
---
## 11. Kilométrage
**Kilométrage inclus :** [●] km par jour / [●] km pour toute la durée de location.
**Kilométrage au départ :** [●] km.
**Kilométrage au retour :** [●] km.
Tout kilomètre supplémentaire sera facturé au tarif de :
**[●] MAD TTC par kilomètre supplémentaire.**
Le kilométrage fait foi sur la base du compteur du véhicule, constaté dans l’état des lieux de départ et de retour.
---
## 12. Utilisation du véhicule
Le Locataire sengage à utiliser le véhicule avec prudence, conformément à sa destination normale et à la législation marocaine.
Il est interdit notamment :
- de sous-louer le véhicule ;
- de transporter des marchandises dangereuses ou illicites ;
- dutiliser le véhicule pour des activités illégales ;
- de participer à des compétitions ou essais sportifs ;
- de tracter un autre véhicule sans autorisation écrite ;
- de circuler hors du territoire marocain sans autorisation écrite du Loueur ;
- de modifier le véhicule ou ses équipements ;
- de fumer dans le véhicule si lagence linterdit ;
- de transporter un nombre de passagers supérieur à celui autorisé.
Le Locataire est responsable des infractions au Code de la route commises pendant la période de location.
---
## 13. Restitution du véhicule
Le Locataire doit restituer le véhicule à la date, à lheure et au lieu prévus au contrat, dans l’état où il lui a été remis, sous réserve de lusure normale.
Le véhicule doit être restitué avec :
- le même niveau de carburant quau départ ;
- les papiers du véhicule ;
- les clés ;
- les accessoires et équipements remis ;
- un état de propreté normal ;
- aucun dommage nouveau non déclaré.
En cas de retard, le Loueur pourra facturer :
- [●] MAD par heure de retard ; ou
- une journée supplémentaire au tarif contractuel au-delà de [●] heures de retard.
En cas de carburant manquant, les frais seront facturés selon le coût réel augmenté de frais de service de [●] MAD.
En cas de salissure excessive, le Loueur pourra facturer des frais de nettoyage de [●] MAD à [●] MAD selon l’état du véhicule.
---
## 14. État des lieux départ et retour
Les Parties établissent un état des lieux au départ et au retour. Celui-ci peut être accompagné de photographies datées du véhicule, du compteur, du carburant, de la carrosserie, de lintérieur, des pneus, des vitres et des équipements.
Les éléments à contrôler comprennent notamment :
- Kilométrage compteur ;
- Niveau de carburant ;
- Carrosserie ;
- Pare-chocs ;
- Rétroviseurs ;
- Phares et feux ;
- Pare-brise et vitres ;
- Pneus et jantes ;
- Intérieur et sièges ;
- Tableau de bord ;
- Roue de secours ;
- Outillage ;
- Documents du véhicule ;
- Clés et accessoires.
Tout dommage constaté au retour et non mentionné dans l’état des lieux de départ pourra être facturé au Locataire, sous réserve des garanties dassurance applicables.
---
## 15. Accident, panne, vol ou sinistre
En cas daccident, panne, vol, tentative de vol, incendie ou tout autre sinistre, le Locataire doit immédiatement :
- informer le Loueur par téléphone et par écrit ;
- prévenir les autorités compétentes si nécessaire ;
- établir un constat amiable en cas daccident ;
- obtenir un procès-verbal ou document officiel en cas de vol, blessure, délit ou dommage important ;
- ne pas abandonner le véhicule sans autorisation du Loueur ;
- ne pas reconnaître de responsabilité sans accord préalable du Loueur ou de lassureur.
Le Locataire doit transmettre au Loueur tous les documents utiles dans un délai maximum de **48 heures** à compter du sinistre.
En cas de non-respect de cette procédure, le Locataire pourra être tenu responsable des conséquences financières, notamment si lassureur refuse sa garantie.
---
## 16. Responsabilité du Locataire
Le Locataire est responsable :
- des dommages causés au véhicule pendant la période de location ;
- des dommages causés aux tiers dans les limites prévues par la loi et lassurance ;
- des amendes, contraventions, frais de fourrière, péages et frais administratifs ;
- des frais liés à une mauvaise utilisation du véhicule ;
- des pertes de clés, documents ou accessoires ;
- des dommages non couverts par lassurance ;
- de la franchise prévue au contrat dassurance.
Le Locataire reste responsable jusqu’à la restitution effective du véhicule au Loueur et la signature de l’état des lieux de retour.
---
## 17. Infractions, amendes et frais administratifs
Toutes les infractions commises pendant la durée de location sont à la charge du Locataire.
Le Loueur pourra communiquer lidentité du Locataire aux autorités compétentes et facturer au Locataire :
- le montant des amendes ;
- les frais de dossier ;
- les frais de fourrière ;
- les frais de notification ;
- tout coût lié au traitement administratif de linfraction.
**Frais administratifs par infraction : [●] MAD TTC.**
---
## 18. Annulation, non-présentation et résiliation
En cas dannulation par le Locataire :
- plus de [●] heures avant le départ : [conditions de remboursement] ;
- moins de [●] heures avant le départ : [conditions] ;
- non-présentation : [conditions].
Le Loueur peut résilier immédiatement le contrat, sans indemnité pour le Locataire, en cas de :
- fausse déclaration ;
- permis invalide ;
- défaut de paiement ;
- utilisation interdite du véhicule ;
- conduite dangereuse ;
- non-respect grave du présent contrat ;
- suspicion légitime de fraude ou dusage illicite.
Dans ce cas, le Locataire doit restituer immédiatement le véhicule.
---
## 19. Indisponibilité administrative, réglementaire ou technique
En cas dindisponibilité du véhicule ou dimpossibilité dexécuter la location pour une raison administrative, réglementaire, technique ou de sécurité, le Loueur pourra proposer au Locataire un véhicule équivalent, sous réserve de disponibilité.
À défaut de véhicule équivalent accepté par le Locataire, les sommes payées au titre de la période non exécutée seront remboursées, sans préjudice des droits légalement reconnus aux Parties.
---
## 20. Données personnelles
Le Locataire autorise le Loueur à collecter et conserver les données nécessaires à lexécution du présent contrat, notamment son identité, ses coordonnées, son permis de conduire, les informations de paiement, les documents liés au véhicule, les états des lieux et les données relatives aux éventuels incidents.
Ces données sont utilisées pour :
- lexécution du contrat ;
- la facturation ;
- la gestion des sinistres ;
- la gestion des infractions ;
- les obligations comptables, fiscales et légales ;
- la défense des droits du Loueur en cas de litige.
Le Loueur sengage à protéger les données personnelles du Locataire, à limiter leur accès aux personnes habilitées et à les conserver uniquement pendant la durée nécessaire aux finalités prévues et aux obligations légales applicables.
---
## 21. Documents annexés
Les documents suivants sont annexés au présent contrat :
- Copie CIN ou passeport du Locataire ;
- Copie du permis de conduire ;
- Copie permis international, le cas échéant ;
- État des lieux de départ ;
- État des lieux de retour ;
- Photos du véhicule au départ ;
- Photos du véhicule au retour ;
- Copie de la carte grise ;
- Copie de lattestation dassurance ;
- Justificatif autorisant lexploitation du véhicule par le Loueur, si le véhicule nappartient pas directement au Loueur ;
- Reçu de paiement ;
- Justificatif de dépôt de garantie ;
- Conditions générales de location, le cas échéant.
Les annexes font partie intégrante du présent contrat.
---
## 22. Loi applicable et juridiction compétente
Le présent contrat est soumis au droit marocain.
En cas de litige relatif à sa validité, son interprétation, son exécution ou sa résiliation, les Parties sefforceront de trouver une solution amiable.
À défaut daccord amiable, le litige sera porté devant le tribunal compétent du ressort de :
**[Ville du siège de lagence / tribunal compétent]**
---
## 23. Déclaration finale
Le Locataire déclare :
- avoir lu et compris le présent contrat ;
- avoir reçu toutes les informations utiles avant signature ;
- avoir vérifié l’état du véhicule au départ ;
- être titulaire dun permis valide ;
- sengager à respecter les conditions du présent contrat ;
- accepter les prix, caution, franchises, pénalités et frais indiqués.
**Fait à :** [●]
**Le :** [●]
**En deux exemplaires originaux.**
Chaque page du présent contrat doit être paraphée par les deux Parties.
### Signature du Loueur
**Nom :** [●]
**Signature et cachet :**
<br><br>
### Signature du Locataire
**Nom :** [●]
**Signature :**
<br><br>
---
# Annexe 1 — État des lieux de départ
**Contrat n° :** [●]
**Date :** [●]
**Heure :** [●]
**Lieu :** [●]
**Véhicule :** [Marque / Modèle]
**Immatriculation :** [●]
**Kilométrage départ :** [●] km
**Carburant départ :** [●]
| Élément | État au départ | Observations |
|---|---|---|
| Carrosserie avant | Bon / Rayé / Endommagé | [●] |
| Carrosserie arrière | Bon / Rayé / Endommagé | [●] |
| Côté droit | Bon / Rayé / Endommagé | [●] |
| Côté gauche | Bon / Rayé / Endommagé | [●] |
| Pare-brise | Bon / Impact / Fissure | [●] |
| Vitres | Bon / Endommagé | [●] |
| Pneus | Bon / Usé / Endommagé | [●] |
| Jantes | Bon / Rayé / Endommagé | [●] |
| Intérieur | Bon / Taché / Endommagé | [●] |
| Sièges | Bon / Taché / Déchiré | [●] |
| Tableau de bord | Bon / Endommagé | [●] |
| Climatisation | Fonctionne / Ne fonctionne pas | [●] |
| Feux | Fonctionnent / Défaut | [●] |
| Roue de secours | Présente / Absente | [●] |
| Outillage | Présent / Absent | [●] |
| Carte grise | Présente / Absente | [●] |
| Assurance | Présente / Absente | [●] |
| Clés | [Nombre] | [●] |
**Photos prises au départ :** Oui / Non
**Nombre de photos :** [●]
**Signature du Loueur :**
<br><br>
**Signature du Locataire :**
<br><br>
---
# Annexe 2 — État des lieux de retour
**Contrat n° :** [●]
**Date :** [●]
**Heure :** [●]
**Lieu :** [●]
**Kilométrage retour :** [●] km
**Kilométrage parcouru :** [●] km
**Kilométrage inclus :** [●] km
**Kilométrage supplémentaire :** [●] km
**Montant km supplémentaires :** [●] MAD
**Carburant retour :** [●]
**Carburant manquant :** Oui / Non
**Montant carburant :** [●] MAD
| Élément | État au retour | Nouveau dommage ? | Observations |
|---|---|---|---|
| Carrosserie avant | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Carrosserie arrière | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Côté droit | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Côté gauche | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Pare-brise | Bon / Impact / Fissure | Oui / Non | [●] |
| Vitres | Bon / Endommagé | Oui / Non | [●] |
| Pneus | Bon / Usé / Endommagé | Oui / Non | [●] |
| Jantes | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Intérieur | Bon / Taché / Endommagé | Oui / Non | [●] |
| Sièges | Bon / Taché / Déchiré | Oui / Non | [●] |
| Documents | Complets / Manquants | Oui / Non | [●] |
| Clés | Restituées / Manquantes | Oui / Non | [●] |
## Frais constatés au retour
| Frais | Montant |
|---|---:|
| Dommages | [●] MAD |
| Franchise assurance | [●] MAD |
| Kilométrage supplémentaire | [●] MAD |
| Carburant | [●] MAD |
| Nettoyage | [●] MAD |
| Retard | [●] MAD |
| Autres frais | [●] MAD |
**Total à retenir sur la caution : [●] MAD**
**Solde de caution à restituer : [●] MAD**
**Photos prises au retour :** Oui / Non
**Nombre de photos :** [●]
**Signature du Loueur :**
<br><br>
**Signature du Locataire :**
<br><br>
---
# Checklist pratique avant signature
- Ne laisser aucun champ `[●]` vide dans la version signée.
- Faire parapher chaque page par les deux Parties.
- Joindre la copie CIN ou passeport du Locataire.
- Joindre la copie du permis de conduire.
- Photographier le compteur et le niveau de carburant au départ et au retour.
- Photographier toutes les faces du véhicule au départ et au retour.
- Écrire clairement le montant de la caution.
- Mentionner la compagnie dassurance, le numéro de police et la franchise.
- Conserver une preuve de paiement.
- Conserver les états des lieux signés.
---
# Note importante
Ce modèle est un document pratique destiné à structurer une location de voiture au Maroc. Il doit être adapté à lactivité réelle du Loueur, à ses conditions dassurance, à son statut fiscal et aux règles applicables. Pour un usage commercial régulier, une validation par un juriste ou avocat marocain est recommandée.
---
# Checklist interne de conformité du Loueur
Cette checklist est destinée au Loueur et ne remplace pas les documents administratifs obligatoires.
- Agrément / autorisation dexercice disponible.
- Responsable réglementaire désigné.
- Casier judiciaire conforme, si exigé.
- Société inscrite à la CNSS, si applicable.
- Siège social ou local professionnel justifié.
- Capital social conforme au seuil applicable.
- Flotte conforme au seuil réglementaire applicable.
- Âge des véhicules conforme selon motorisation.
- Documents de propriété ou dexploitation des véhicules disponibles.
- Assurance et visite technique valides pour chaque véhicule.
- Procédure de déclaration en cas de suspension ou cessation dactivité.
+699
View File
@@ -0,0 +1,699 @@
# Contrat de Location de Véhicule
**Contrat n° :** [●]
**Date de signature :** [●]
**Lieu de signature :** [Ville, Maroc]
---
## 1. Parties au contrat
Entre les soussignés :
### Le Loueur / Agence
**Nom commercial :** [●]
**Raison sociale :** [●]
**Forme juridique :** [●]
**RC n° :** [●]
**ICE n° :** [●]
**IF n° :** [●]
**Agrément / Autorisation dexercice n° :** [●]
**Date de délivrance de lagrément :** [●]
**Autorité délivrante :** [●]
**Adresse :** [●]
**Téléphone :** [●]
**Email :** [●]
**Représenté par :** [Nom, prénom, fonction]
Ci-après désigné **« le Loueur »**,
Et :
### Le Locataire / Conducteur principal
**Nom :** [●]
**Prénom :** [●]
**Date de naissance :** [●]
**Nationalité :** [●]
**CIN / Passeport n° :** [●]
**Adresse complète :** [●]
**Téléphone :** [●]
**Email :** [●]
Ci-après désigné **« le Locataire »**.
Le Loueur et le Locataire sont ensemble désignés **« les Parties »**.
---
## 2. Responsable légal et conformité réglementaire du Loueur
Le Loueur déclare exercer lactivité de location de véhicules sans chauffeur conformément à la réglementation marocaine applicable.
Le Loueur reconnaît que son activité doit respecter les textes mentionnés dans la section **Références légales et réglementaires applicables** du présent document.
La personne responsable de lactivité est :
**Nom et prénom :** [●]
**Qualité :** [Dirigeant / Actionnaire / Salarié]
**Pièce didentité n° :** [●]
**Qualification / diplôme / expérience :** [●]
**Téléphone :** [●]
**Email :** [●]
Cette personne est responsable du suivi des contrats, de lentretien des véhicules, de leur conformité aux normes de sécurité routière et du respect des obligations administratives applicables.
Le Loueur déclare notamment :
- disposer des autorisations administratives requises pour lexercice de lactivité ;
- être régulièrement immatriculé auprès des administrations compétentes ;
- disposer dun siège social ou local professionnel déclaré ;
- respecter les obligations sociales, fiscales et administratives applicables ;
- exploiter une flotte conforme aux seuils et conditions réglementaires applicables ;
- maintenir les véhicules loués en état conforme aux normes de sécurité routière.
---
## 3. Permis de conduire
Le Locataire déclare être titulaire dun permis de conduire valide :
**Numéro du permis :** [●]
**Catégorie :** [B / autre]
**Date de délivrance :** [●]
**Date dexpiration :** [●]
**Pays de délivrance :** [●]
**Permis international, le cas échéant :** [Oui / Non, n° ●]
Le Locataire garantit que son permis est valide pendant toute la durée de location et quil nest frappé daucune suspension, annulation ou restriction incompatible avec la conduite du véhicule loué.
---
## 4. Conducteur(s) autorisé(s)
Le véhicule ne peut être conduit que par le Locataire et, le cas échéant, par les conducteurs additionnels expressément mentionnés ci-dessous :
### Conducteur additionnel 1
**Nom et prénom :** [●]
**CIN / Passeport :** [●]
**Permis n° :** [●]
**Catégorie :** [●]
**Date de délivrance :** [●]
**Téléphone :** [●]
Tout conducteur non déclaré est interdit. En cas daccident, vol, infraction ou dommage causé par un conducteur non autorisé, le Locataire assume lentière responsabilité des conséquences financières, civiles, pénales et assurantielles.
---
## 5. Véhicule loué
Le Loueur met à disposition du Locataire le véhicule suivant :
**Marque :** [●]
**Modèle :** [●]
**Année :** [●]
**Couleur :** [●]
**Immatriculation :** [●]
**Numéro de châssis :** [●]
**Propriétaire du véhicule :** [Loueur / Autre agence / Société de financement / Autre]
**Justificatif dexploitation si le véhicule appartient à un tiers :** [Contrat / Autorisation / Mandat / Autre]
**Type de motorisation :** [Thermique / Hybride / Électrique]
**Date de première mise en circulation :** [●]
**Âge du véhicule à la date de location :** [●]
**Véhicule conforme aux limites réglementaires dexploitation :** [Oui / Non]
**Kilométrage au départ :** [●] km
**Niveau de carburant au départ :** [●]
**Carte grise :** [Oui / Non]
**Assurance :** [Oui / Non]
**Visite technique :** [Oui / Non / Non applicable]
Un état des lieux contradictoire au départ et au retour est annexé au présent contrat et en fait partie intégrante.
Le Loueur déclare que le véhicule mis à disposition est conforme aux conditions réglementaires dexploitation applicables à sa catégorie, notamment en matière d’âge, dimmatriculation, dassurance, dentretien et de sécurité routière.
---
## 6. Entretien et conformité du véhicule
Le Loueur déclare que le véhicule est régulièrement entretenu, assuré, immatriculé et conforme aux normes de sécurité routière applicables.
Le Locataire reconnaît avoir reçu un véhicule en état apparent de fonctionnement, sous réserve des observations mentionnées dans l’état des lieux de départ.
En cas danomalie mécanique, voyant dalerte, bruit anormal, crevaison, panne ou incident susceptible daffecter la sécurité, le Locataire doit cesser lutilisation du véhicule dès que les conditions de sécurité le permettent et informer immédiatement le Loueur.
---
## 7. Durée de location
La location commence le :
**Date :** [●]
**Heure :** [●]
**Lieu de prise en charge :** [●]
La location prend fin le :
**Date :** [●]
**Heure :** [●]
**Lieu de restitution :** [●]
Toute prolongation doit être acceptée préalablement par écrit par le Loueur. À défaut, le véhicule sera considéré comme conservé sans autorisation, et le Loueur pourra facturer les jours supplémentaires, pénalités de retard et frais éventuels.
---
## 8. Prix de location et modalités de paiement
Le prix de location est fixé comme suit :
| Désignation | Montant |
|---|---:|
| Tarif journalier HT | [●] MAD |
| Nombre de jours | [●] |
| Sous-total HT | [●] MAD |
| TVA applicable | [●] % |
| Montant TVA | [●] MAD |
| Total TTC | [●] MAD |
| Conducteur additionnel | [●] MAD |
| Livraison / récupération | [●] MAD |
| Siège enfant | [●] MAD |
| GPS / accessoires | [●] MAD |
| Autres frais | [●] MAD |
**Montant total à payer TTC : [●] MAD**
**Mode de paiement :** [Espèces / carte bancaire / virement / chèque / autre]
**Date de paiement :** [●]
**Référence de paiement :** [●]
Le Locataire reconnaît avoir reçu une information claire sur le prix total TTC avant la signature du contrat.
---
## 9. Dépôt de garantie / caution
Le Locataire verse au Loueur un dépôt de garantie de :
**Montant : [●] MAD**
**Forme de la caution :** [Préautorisation bancaire / espèces / chèque / autre]
**Date de constitution :** [●]
**Conditions de libération :** [●]
Le dépôt de garantie garantit notamment :
- les dommages non couverts par lassurance ;
- la franchise dassurance ;
- les kilomètres supplémentaires ;
- le carburant manquant ;
- les frais de nettoyage exceptionnel ;
- les contraventions, amendes, péages ou frais administratifs ;
- les retards de restitution ;
- la perte de documents, clés, accessoires ou équipements.
La caution sera restituée après vérification de l’état du véhicule, du kilométrage, du carburant, des éventuelles infractions et des sommes restant dues.
---
## 10. Assurance
Le véhicule est assuré auprès de :
**Compagnie dassurance :** [●]
**Numéro de police :** [●]
**Type de couverture :** [Responsabilité civile / Tous risques / autre]
**Franchise applicable :** [●] MAD
**Assistance :** [Oui / Non]
**Numéro dassistance :** [●]
Le Locataire reconnaît avoir été informé des garanties, exclusions et franchises applicables.
Sont notamment exclus de la couverture, sauf stipulation contraire de lassureur :
- conduite par une personne non autorisée ;
- conduite sans permis valide ;
- conduite sous lemprise dalcool, stupéfiants ou substances interdites ;
- usage du véhicule hors des voies autorisées ;
- participation à des courses, essais ou compétitions ;
- négligence grave ;
- fausse déclaration ;
- absence de déclaration daccident dans les délais ;
- vol avec clés laissées dans le véhicule ;
- transport illicite de personnes ou de marchandises.
---
## 11. Kilométrage
**Kilométrage inclus :** [●] km par jour / [●] km pour toute la durée de location.
**Kilométrage au départ :** [●] km.
**Kilométrage au retour :** [●] km.
Tout kilomètre supplémentaire sera facturé au tarif de :
**[●] MAD TTC par kilomètre supplémentaire.**
Le kilométrage fait foi sur la base du compteur du véhicule, constaté dans l’état des lieux de départ et de retour.
---
## 12. Utilisation du véhicule
Le Locataire sengage à utiliser le véhicule avec prudence, conformément à sa destination normale et à la législation marocaine.
Il est interdit notamment :
- de sous-louer le véhicule ;
- de transporter des marchandises dangereuses ou illicites ;
- dutiliser le véhicule pour des activités illégales ;
- de participer à des compétitions ou essais sportifs ;
- de tracter un autre véhicule sans autorisation écrite ;
- de circuler hors du territoire marocain sans autorisation écrite du Loueur ;
- de modifier le véhicule ou ses équipements ;
- de fumer dans le véhicule si lagence linterdit ;
- de transporter un nombre de passagers supérieur à celui autorisé.
Le Locataire est responsable des infractions au Code de la route commises pendant la période de location.
---
## 13. Restitution du véhicule
Le Locataire doit restituer le véhicule à la date, à lheure et au lieu prévus au contrat, dans l’état où il lui a été remis, sous réserve de lusure normale.
Le véhicule doit être restitué avec :
- le même niveau de carburant quau départ ;
- les papiers du véhicule ;
- les clés ;
- les accessoires et équipements remis ;
- un état de propreté normal ;
- aucun dommage nouveau non déclaré.
En cas de retard, le Loueur pourra facturer :
- [●] MAD par heure de retard ; ou
- une journée supplémentaire au tarif contractuel au-delà de [●] heures de retard.
En cas de carburant manquant, les frais seront facturés selon le coût réel augmenté de frais de service de [●] MAD.
En cas de salissure excessive, le Loueur pourra facturer des frais de nettoyage de [●] MAD à [●] MAD selon l’état du véhicule.
---
## 14. État des lieux départ et retour
Les Parties établissent un état des lieux au départ et au retour. Celui-ci peut être accompagné de photographies datées du véhicule, du compteur, du carburant, de la carrosserie, de lintérieur, des pneus, des vitres et des équipements.
Les éléments à contrôler comprennent notamment :
- Kilométrage compteur ;
- Niveau de carburant ;
- Carrosserie ;
- Pare-chocs ;
- Rétroviseurs ;
- Phares et feux ;
- Pare-brise et vitres ;
- Pneus et jantes ;
- Intérieur et sièges ;
- Tableau de bord ;
- Roue de secours ;
- Outillage ;
- Documents du véhicule ;
- Clés et accessoires.
Tout dommage constaté au retour et non mentionné dans l’état des lieux de départ pourra être facturé au Locataire, sous réserve des garanties dassurance applicables.
---
## 15. Accident, panne, vol ou sinistre
En cas daccident, panne, vol, tentative de vol, incendie ou tout autre sinistre, le Locataire doit immédiatement :
- informer le Loueur par téléphone et par écrit ;
- prévenir les autorités compétentes si nécessaire ;
- établir un constat amiable en cas daccident ;
- obtenir un procès-verbal ou document officiel en cas de vol, blessure, délit ou dommage important ;
- ne pas abandonner le véhicule sans autorisation du Loueur ;
- ne pas reconnaître de responsabilité sans accord préalable du Loueur ou de lassureur.
Le Locataire doit transmettre au Loueur tous les documents utiles dans un délai maximum de **48 heures** à compter du sinistre.
En cas de non-respect de cette procédure, le Locataire pourra être tenu responsable des conséquences financières, notamment si lassureur refuse sa garantie.
---
## 16. Responsabilité du Locataire
Le Locataire est responsable :
- des dommages causés au véhicule pendant la période de location ;
- des dommages causés aux tiers dans les limites prévues par la loi et lassurance ;
- des amendes, contraventions, frais de fourrière, péages et frais administratifs ;
- des frais liés à une mauvaise utilisation du véhicule ;
- des pertes de clés, documents ou accessoires ;
- des dommages non couverts par lassurance ;
- de la franchise prévue au contrat dassurance.
Le Locataire reste responsable jusqu’à la restitution effective du véhicule au Loueur et la signature de l’état des lieux de retour.
---
## 17. Infractions, amendes et frais administratifs
Toutes les infractions commises pendant la durée de location sont à la charge du Locataire.
Le Loueur pourra communiquer lidentité du Locataire aux autorités compétentes et facturer au Locataire :
- le montant des amendes ;
- les frais de dossier ;
- les frais de fourrière ;
- les frais de notification ;
- tout coût lié au traitement administratif de linfraction.
**Frais administratifs par infraction : [●] MAD TTC.**
---
## 18. Annulation, non-présentation et résiliation
En cas dannulation par le Locataire :
- plus de [●] heures avant le départ : [conditions de remboursement] ;
- moins de [●] heures avant le départ : [conditions] ;
- non-présentation : [conditions].
Le Loueur peut résilier immédiatement le contrat, sans indemnité pour le Locataire, en cas de :
- fausse déclaration ;
- permis invalide ;
- défaut de paiement ;
- utilisation interdite du véhicule ;
- conduite dangereuse ;
- non-respect grave du présent contrat ;
- suspicion légitime de fraude ou dusage illicite.
Dans ce cas, le Locataire doit restituer immédiatement le véhicule.
---
## 19. Indisponibilité administrative, réglementaire ou technique
En cas dindisponibilité du véhicule ou dimpossibilité dexécuter la location pour une raison administrative, réglementaire, technique ou de sécurité, le Loueur pourra proposer au Locataire un véhicule équivalent, sous réserve de disponibilité.
À défaut de véhicule équivalent accepté par le Locataire, les sommes payées au titre de la période non exécutée seront remboursées, sans préjudice des droits légalement reconnus aux Parties.
---
## 20. Données personnelles
Le Locataire autorise le Loueur à collecter et conserver les données nécessaires à lexécution du présent contrat, notamment son identité, ses coordonnées, son permis de conduire, les informations de paiement, les documents liés au véhicule, les états des lieux et les données relatives aux éventuels incidents.
Ces données sont utilisées pour :
- lexécution du contrat ;
- la facturation ;
- la gestion des sinistres ;
- la gestion des infractions ;
- les obligations comptables, fiscales et légales ;
- la défense des droits du Loueur en cas de litige.
Le Loueur sengage à protéger les données personnelles du Locataire, à limiter leur accès aux personnes habilitées et à les conserver uniquement pendant la durée nécessaire aux finalités prévues et aux obligations légales applicables.
---
## 21. Documents annexés
Les documents suivants sont annexés au présent contrat :
- Copie CIN ou passeport du Locataire ;
- Copie du permis de conduire ;
- Copie permis international, le cas échéant ;
- État des lieux de départ ;
- État des lieux de retour ;
- Photos du véhicule au départ ;
- Photos du véhicule au retour ;
- Copie de la carte grise ;
- Copie de lattestation dassurance ;
- Justificatif autorisant lexploitation du véhicule par le Loueur, si le véhicule nappartient pas directement au Loueur ;
- Reçu de paiement ;
- Justificatif de dépôt de garantie ;
- Conditions générales de location, le cas échéant.
- Liste des références légales et réglementaires applicables ;
Les annexes font partie intégrante du présent contrat.
---
## 22. Loi applicable et juridiction compétente
Le présent contrat est soumis au droit marocain.
En cas de litige relatif à sa validité, son interprétation, son exécution ou sa résiliation, les Parties sefforceront de trouver une solution amiable.
À défaut daccord amiable, le litige sera porté devant le tribunal compétent du ressort de :
**[Ville du siège de lagence / tribunal compétent]**
---
## 23. Déclaration finale
Le Locataire déclare :
- avoir lu et compris le présent contrat ;
- avoir reçu toutes les informations utiles avant signature ;
- avoir vérifié l’état du véhicule au départ ;
- être titulaire dun permis valide ;
- sengager à respecter les conditions du présent contrat ;
- accepter les prix, caution, franchises, pénalités et frais indiqués.
**Fait à :** [●]
**Le :** [●]
**En deux exemplaires originaux.**
Chaque page du présent contrat doit être paraphée par les deux Parties.
### Signature du Loueur
**Nom :** [●]
**Signature et cachet :**
<br><br>
### Signature du Locataire
**Nom :** [●]
**Signature :**
<br><br>
---
# Annexe 1 — État des lieux de départ
**Contrat n° :** [●]
**Date :** [●]
**Heure :** [●]
**Lieu :** [●]
**Véhicule :** [Marque / Modèle]
**Immatriculation :** [●]
**Kilométrage départ :** [●] km
**Carburant départ :** [●]
| Élément | État au départ | Observations |
|---|---|---|
| Carrosserie avant | Bon / Rayé / Endommagé | [●] |
| Carrosserie arrière | Bon / Rayé / Endommagé | [●] |
| Côté droit | Bon / Rayé / Endommagé | [●] |
| Côté gauche | Bon / Rayé / Endommagé | [●] |
| Pare-brise | Bon / Impact / Fissure | [●] |
| Vitres | Bon / Endommagé | [●] |
| Pneus | Bon / Usé / Endommagé | [●] |
| Jantes | Bon / Rayé / Endommagé | [●] |
| Intérieur | Bon / Taché / Endommagé | [●] |
| Sièges | Bon / Taché / Déchiré | [●] |
| Tableau de bord | Bon / Endommagé | [●] |
| Climatisation | Fonctionne / Ne fonctionne pas | [●] |
| Feux | Fonctionnent / Défaut | [●] |
| Roue de secours | Présente / Absente | [●] |
| Outillage | Présent / Absent | [●] |
| Carte grise | Présente / Absente | [●] |
| Assurance | Présente / Absente | [●] |
| Clés | [Nombre] | [●] |
**Photos prises au départ :** Oui / Non
**Nombre de photos :** [●]
**Signature du Loueur :**
<br><br>
**Signature du Locataire :**
<br><br>
---
# Annexe 2 — État des lieux de retour
**Contrat n° :** [●]
**Date :** [●]
**Heure :** [●]
**Lieu :** [●]
**Kilométrage retour :** [●] km
**Kilométrage parcouru :** [●] km
**Kilométrage inclus :** [●] km
**Kilométrage supplémentaire :** [●] km
**Montant km supplémentaires :** [●] MAD
**Carburant retour :** [●]
**Carburant manquant :** Oui / Non
**Montant carburant :** [●] MAD
| Élément | État au retour | Nouveau dommage ? | Observations |
|---|---|---|---|
| Carrosserie avant | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Carrosserie arrière | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Côté droit | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Côté gauche | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Pare-brise | Bon / Impact / Fissure | Oui / Non | [●] |
| Vitres | Bon / Endommagé | Oui / Non | [●] |
| Pneus | Bon / Usé / Endommagé | Oui / Non | [●] |
| Jantes | Bon / Rayé / Endommagé | Oui / Non | [●] |
| Intérieur | Bon / Taché / Endommagé | Oui / Non | [●] |
| Sièges | Bon / Taché / Déchiré | Oui / Non | [●] |
| Documents | Complets / Manquants | Oui / Non | [●] |
| Clés | Restituées / Manquantes | Oui / Non | [●] |
## Frais constatés au retour
| Frais | Montant |
|---|---:|
| Dommages | [●] MAD |
| Franchise assurance | [●] MAD |
| Kilométrage supplémentaire | [●] MAD |
| Carburant | [●] MAD |
| Nettoyage | [●] MAD |
| Retard | [●] MAD |
| Autres frais | [●] MAD |
**Total à retenir sur la caution : [●] MAD**
**Solde de caution à restituer : [●] MAD**
**Photos prises au retour :** Oui / Non
**Nombre de photos :** [●]
**Signature du Loueur :**
<br><br>
**Signature du Locataire :**
<br><br>
---
# Checklist pratique avant signature
- Ne laisser aucun champ `[●]` vide dans la version signée.
- Faire parapher chaque page par les deux Parties.
- Joindre la copie CIN ou passeport du Locataire.
- Joindre la copie du permis de conduire.
- Photographier le compteur et le niveau de carburant au départ et au retour.
- Photographier toutes les faces du véhicule au départ et au retour.
- Écrire clairement le montant de la caution.
- Mentionner la compagnie dassurance, le numéro de police et la franchise.
- Conserver une preuve de paiement.
- Conserver les états des lieux signés.
---
# Note importante
Ce modèle est un document pratique destiné à structurer une location de voiture au Maroc. Il doit être adapté à lactivité réelle du Loueur, à ses conditions dassurance, à son statut fiscal et aux règles applicables. Pour un usage commercial régulier, une validation par un juriste ou avocat marocain est recommandée.
---
# Références légales et réglementaires applicables
Les références suivantes sont indiquées à titre de base juridique générale pour lactivité de location de véhicules sans chauffeur au Maroc. Elles doivent être vérifiées et actualisées selon les textes officiels en vigueur, notamment le Bulletin Officiel et les circulaires ou cahiers des charges applicables.
## Textes principaux relatifs à la location de véhicules
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Dahir n° 1-63-260 du 12 novembre 1963** | Transports par véhicules automobiles sur route | Cadre général applicable au transport routier et aux activités connexes. |
| **Décret n° 2.69.351 du 4 avril 1970** | Conditions dexploitation des voitures louées sans chauffeur | Texte spécifique encadrant lexploitation des véhicules loués sans chauffeur. |
| **Cahier des charges relatif à la location de voitures sans chauffeur, tel quen vigueur** | Conditions administratives, techniques et professionnelles de lactivité | Référence pratique pour lagrément, la flotte, le responsable dactivité, l’âge des véhicules, les obligations administratives et les sanctions. |
## Circulation, sécurité routière et véhicules
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Loi n° 52-05 portant Code de la route** | Règles de circulation, infractions, sécurité routière, véhicules et conducteurs | Encadre lusage du véhicule, les infractions, la responsabilité du conducteur et les obligations de sécurité. |
| **Dahir n° 1-10-07 du 11 février 2010** | Promulgation de la Loi n° 52-05 portant Code de la route | Texte de promulgation du Code de la route. |
## Assurance, responsabilité et obligations contractuelles
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Loi n° 17-99 portant Code des assurances** | Assurance automobile, garanties, exclusions, franchises et responsabilité assurantielle | Encadre les assurances liées au véhicule loué, les garanties, les exclusions et les franchises. |
| **Dahir des Obligations et des Contrats du 12 août 1913** | Règles générales des contrats, obligations, responsabilité civile, consentement et inexécution | Base générale du contrat de location et de la responsabilité des Parties. |
## Droit commercial et structure de lagence
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Loi n° 15-95 formant Code de commerce** | Activité commerciale, commerçants, obligations commerciales, registre de commerce | Applicable à lactivité commerciale du Loueur. |
| **Loi n° 5-96** | Sociétés à responsabilité limitée, sociétés en nom collectif, sociétés en commandite simple, sociétés en commandite par actions et sociétés en participation | Applicable si le Loueur est constitué sous lune de ces formes, notamment SARL. |
| **Loi n° 17-95 relative aux sociétés anonymes** | Sociétés anonymes | Applicable si le Loueur est constitué sous forme de société anonyme. |
| **Loi n° 69-21 relative aux délais de paiement** | Délais de paiement entre professionnels | Applicable notamment aux locations ou relations commerciales B2B. |
## Protection du consommateur, données personnelles et signature électronique
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Loi n° 31-08 édictant des mesures de protection du consommateur** | Information du consommateur, transparence des prix, clauses abusives, droits du client | Applicable lorsque le Locataire agit comme consommateur. |
| **Loi n° 09-08 relative à la protection des personnes physiques à l’égard du traitement des données à caractère personnel** | Collecte, traitement, conservation et protection des données personnelles | Applicable aux données du Locataire : CIN, passeport, permis, coordonnées, signatures, documents et photos. |
| **Loi n° 53-05 relative à l’échange électronique de données juridiques** | Documents électroniques, signature électronique et valeur juridique des échanges numériques | Applicable si le contrat est signé, transmis ou conservé électroniquement. |
## Obligations sociales, fiscales et administratives
| Référence | Objet | Utilité pour le présent contrat |
|---|---|---|
| **Dahir n° 1-72-184 relatif au régime de sécurité sociale** | Régime CNSS et obligations sociales | Pertinent pour la conformité sociale du Loueur lorsquil emploie du personnel. |
| **Loi n° 47-06 relative à la fiscalité des collectivités locales** | Fiscalité locale, taxe professionnelle et obligations locales | Pertinente pour les obligations fiscales locales de lagence. |
| **Code Général des Impôts** | TVA, impôt sur les sociétés, impôt sur le revenu, facturation et obligations fiscales | Pertinent pour la facturation, les déclarations fiscales et le traitement de la TVA. |
## Clause de prudence juridique
Le présent contrat doit être interprété et appliqué conformément aux textes marocains en vigueur à la date de signature.
Lorsque le présent contrat mentionne le **cahier des charges relatif à la location de voitures sans chauffeur**, cette référence vise le cahier des charges applicable tel quen vigueur, y compris ses modifications, circulaires dapplication ou textes complémentaires.
En cas de contradiction entre le présent contrat et une disposition légale ou réglementaire impérative, la disposition légale ou réglementaire prévaut. Les autres clauses du contrat demeurent applicables dans la mesure permise par la loi.
---
# Checklist interne de conformité du Loueur
Cette checklist est destinée au Loueur et ne remplace pas les documents administratifs obligatoires.
- Agrément / autorisation dexercice disponible.
- Responsable réglementaire désigné.
- Casier judiciaire conforme, si exigé.
- Société inscrite à la CNSS, si applicable.
- Siège social ou local professionnel justifié.
- Capital social conforme au seuil applicable.
- Flotte conforme au seuil réglementaire applicable.
- Âge des véhicules conforme selon motorisation.
- Documents de propriété ou dexploitation des véhicules disponibles.
- Assurance et visite technique valides pour chaque véhicule.
- Procédure de déclaration en cas de suspension ou cessation dactivité.
@@ -0,0 +1,493 @@
CREATE TYPE "BillingInvoiceTerms" AS ENUM (
'DUE_ON_RECEIPT',
'NET_7',
'NET_15',
'NET_30',
'NET_45',
'NET_60'
);
CREATE TYPE "BillingInvoiceStatus" AS ENUM (
'DRAFT',
'OPEN',
'PAID',
'PARTIALLY_PAID',
'PAYMENT_PENDING',
'PAST_DUE',
'VOID',
'UNCOLLECTIBLE',
'REFUNDED',
'PARTIALLY_REFUNDED'
);
CREATE TYPE "BillingInvoiceType" AS ENUM (
'SUBSCRIPTION_INITIAL',
'SUBSCRIPTION_RENEWAL',
'TRIAL_CONVERSION',
'SUBSCRIPTION_UPGRADE',
'SUBSCRIPTION_DOWNGRADE_CREDIT',
'USAGE_BASED',
'ONE_TIME',
'MANUAL',
'CORRECTION',
'CANCELLATION_FEE'
);
CREATE TYPE "BillingLineItemType" AS ENUM (
'SUBSCRIPTION_FEE',
'SEAT_FEE',
'USAGE_FEE',
'SETUP_FEE',
'DISCOUNT',
'TAX',
'CREDIT',
'PRORATION',
'REFUND_ADJUSTMENT',
'MANUAL_ADJUSTMENT',
'PROFESSIONAL_SERVICES',
'CANCELLATION_FEE'
);
CREATE TYPE "BillingPaymentMethodType" AS ENUM (
'CARD',
'ACH_DEBIT',
'BANK_TRANSFER',
'WIRE_TRANSFER',
'MANUAL_INVOICE',
'PURCHASE_ORDER'
);
CREATE TYPE "BillingPaymentIntentStatus" AS ENUM (
'REQUIRES_PAYMENT_METHOD',
'REQUIRES_CONFIRMATION',
'REQUIRES_ACTION',
'PROCESSING',
'SUCCEEDED',
'FAILED',
'CANCELED',
'REFUNDED',
'PARTIALLY_REFUNDED'
);
CREATE TYPE "BillingPaymentAttemptStatus" AS ENUM (
'PENDING',
'PROCESSING',
'SUCCEEDED',
'FAILED',
'CANCELED',
'REFUNDED',
'PARTIALLY_REFUNDED'
);
CREATE TYPE "BillingRefundStatus" AS ENUM (
'PENDING',
'SUCCEEDED',
'FAILED'
);
CREATE TYPE "BillingCreditNoteStatus" AS ENUM (
'ISSUED',
'APPLIED',
'VOID'
);
CREATE SEQUENCE "billing_invoice_sequence_seq" START 1;
CREATE TABLE "billing_accounts" (
"id" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"isPrimary" BOOLEAN NOT NULL DEFAULT true,
"legalName" TEXT NOT NULL,
"billingEmail" TEXT NOT NULL,
"billingAddress" JSONB,
"taxId" TEXT,
"taxExempt" BOOLEAN NOT NULL DEFAULT false,
"defaultCurrency" TEXT NOT NULL DEFAULT 'MAD',
"defaultPaymentMethodId" TEXT,
"invoiceTerms" "BillingInvoiceTerms" NOT NULL DEFAULT 'DUE_ON_RECEIPT',
"netTermsDays" INTEGER NOT NULL DEFAULT 0,
"providerCustomerId" TEXT,
"dunningPaused" BOOLEAN NOT NULL DEFAULT false,
"dunningPausedAt" TIMESTAMP(3),
"dunningPausedBy" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_accounts_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_payment_methods" (
"id" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"type" "BillingPaymentMethodType" NOT NULL,
"providerPaymentMethodId" TEXT,
"label" TEXT,
"brand" TEXT,
"last4" TEXT,
"expMonth" INTEGER,
"expYear" INTEGER,
"isDefault" BOOLEAN NOT NULL DEFAULT false,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_payment_methods_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_invoices" (
"id" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"subscriptionId" TEXT,
"invoiceNumber" TEXT,
"invoiceSequence" INTEGER,
"invoiceType" "BillingInvoiceType" NOT NULL,
"status" "BillingInvoiceStatus" NOT NULL DEFAULT 'DRAFT',
"currency" TEXT NOT NULL DEFAULT 'MAD',
"subtotalAmount" INTEGER NOT NULL DEFAULT 0,
"discountAmount" INTEGER NOT NULL DEFAULT 0,
"creditAmount" INTEGER NOT NULL DEFAULT 0,
"taxAmount" INTEGER NOT NULL DEFAULT 0,
"totalAmount" INTEGER NOT NULL DEFAULT 0,
"amountPaid" INTEGER NOT NULL DEFAULT 0,
"amountDue" INTEGER NOT NULL DEFAULT 0,
"invoiceDate" TIMESTAMP(3),
"dueAt" TIMESTAMP(3),
"finalizedAt" TIMESTAMP(3),
"paidAt" TIMESTAMP(3),
"voidedAt" TIMESTAMP(3),
"markedUncollectibleAt" TIMESTAMP(3),
"billingName" TEXT,
"billingEmail" TEXT,
"billingAddress" JSONB,
"providerInvoiceId" TEXT,
"paymentProvider" "PaymentProvider",
"isSubscriptionBlocking" BOOLEAN NOT NULL DEFAULT false,
"adminReason" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdByAdminId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_invoices_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_invoice_line_items" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"subscriptionId" TEXT,
"plan" "Plan",
"type" "BillingLineItemType" NOT NULL,
"description" TEXT NOT NULL,
"quantity" INTEGER NOT NULL DEFAULT 1,
"unitAmount" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"periodStart" TIMESTAMP(3),
"periodEnd" TIMESTAMP(3),
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_invoice_line_items_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_payment_intents" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"paymentMethodId" TEXT,
"providerPaymentIntentId" TEXT,
"status" "BillingPaymentIntentStatus" NOT NULL DEFAULT 'REQUIRES_PAYMENT_METHOD',
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_payment_intents_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_payment_attempts" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"paymentIntentId" TEXT,
"paymentMethodId" TEXT,
"providerPaymentId" TEXT,
"status" "BillingPaymentAttemptStatus" NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"failureCode" TEXT,
"failureMessage" TEXT,
"attemptedAt" TIMESTAMP(3) NOT NULL,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_payment_attempts_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_credit_balances" (
"id" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"balanceAmount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_credit_balances_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_credit_ledger_entries" (
"id" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"invoiceId" TEXT,
"type" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"reason" TEXT NOT NULL,
"createdBy" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_credit_ledger_entries_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_credit_notes" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"billingAccountId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"reason" TEXT NOT NULL,
"status" "BillingCreditNoteStatus" NOT NULL DEFAULT 'ISSUED',
"createdBy" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_credit_notes_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_refunds" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"paymentAttemptId" TEXT,
"billingAccountId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'MAD',
"reason" TEXT NOT NULL,
"status" "BillingRefundStatus" NOT NULL DEFAULT 'PENDING',
"providerRefundId" TEXT,
"createdBy" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_refunds_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_tax_records" (
"id" TEXT NOT NULL,
"invoiceId" TEXT NOT NULL,
"jurisdiction" TEXT,
"taxRate" DOUBLE PRECISION,
"taxAmount" INTEGER NOT NULL,
"taxType" TEXT,
"taxExempt" BOOLEAN NOT NULL DEFAULT false,
"exemptionReason" TEXT,
"providerTaxId" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_tax_records_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "billing_events" (
"id" TEXT NOT NULL,
"billingAccountId" TEXT,
"invoiceId" TEXT,
"subscriptionId" TEXT,
"companyId" TEXT,
"eventType" TEXT NOT NULL,
"source" TEXT NOT NULL,
"payload" JSONB NOT NULL DEFAULT '{}',
"occurredAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_events_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "subscription_invoices"
ADD COLUMN "billingInvoiceId" TEXT;
CREATE UNIQUE INDEX "subscription_invoices_billingInvoiceId_key" ON "subscription_invoices"("billingInvoiceId");
CREATE UNIQUE INDEX "billing_invoices_invoiceNumber_key" ON "billing_invoices"("invoiceNumber");
CREATE UNIQUE INDEX "billing_invoices_invoiceSequence_key" ON "billing_invoices"("invoiceSequence");
CREATE UNIQUE INDEX "billing_credit_balances_billingAccountId_currency_key" ON "billing_credit_balances"("billingAccountId", "currency");
CREATE INDEX "billing_accounts_companyId_idx" ON "billing_accounts"("companyId");
CREATE INDEX "billing_accounts_companyId_isPrimary_idx" ON "billing_accounts"("companyId", "isPrimary");
CREATE INDEX "billing_payment_methods_billingAccountId_idx" ON "billing_payment_methods"("billingAccountId");
CREATE INDEX "billing_invoices_billingAccountId_idx" ON "billing_invoices"("billingAccountId");
CREATE INDEX "billing_invoices_companyId_idx" ON "billing_invoices"("companyId");
CREATE INDEX "billing_invoices_subscriptionId_idx" ON "billing_invoices"("subscriptionId");
CREATE INDEX "billing_invoice_line_items_invoiceId_idx" ON "billing_invoice_line_items"("invoiceId");
CREATE INDEX "billing_payment_intents_invoiceId_idx" ON "billing_payment_intents"("invoiceId");
CREATE INDEX "billing_payment_intents_billingAccountId_idx" ON "billing_payment_intents"("billingAccountId");
CREATE INDEX "billing_payment_attempts_invoiceId_idx" ON "billing_payment_attempts"("invoiceId");
CREATE INDEX "billing_payment_attempts_billingAccountId_idx" ON "billing_payment_attempts"("billingAccountId");
CREATE INDEX "billing_credit_ledger_entries_billingAccountId_idx" ON "billing_credit_ledger_entries"("billingAccountId");
CREATE INDEX "billing_credit_notes_invoiceId_idx" ON "billing_credit_notes"("invoiceId");
CREATE INDEX "billing_credit_notes_billingAccountId_idx" ON "billing_credit_notes"("billingAccountId");
CREATE INDEX "billing_refunds_invoiceId_idx" ON "billing_refunds"("invoiceId");
CREATE INDEX "billing_refunds_billingAccountId_idx" ON "billing_refunds"("billingAccountId");
CREATE INDEX "billing_tax_records_invoiceId_idx" ON "billing_tax_records"("invoiceId");
CREATE INDEX "billing_events_billingAccountId_idx" ON "billing_events"("billingAccountId");
CREATE INDEX "billing_events_invoiceId_idx" ON "billing_events"("invoiceId");
CREATE INDEX "billing_events_subscriptionId_idx" ON "billing_events"("subscriptionId");
CREATE INDEX "billing_events_companyId_idx" ON "billing_events"("companyId");
ALTER TABLE "billing_accounts"
ADD CONSTRAINT "billing_accounts_companyId_fkey"
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_methods"
ADD CONSTRAINT "billing_payment_methods_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_accounts"
ADD CONSTRAINT "billing_accounts_defaultPaymentMethodId_fkey"
FOREIGN KEY ("defaultPaymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_invoices"
ADD CONSTRAINT "billing_invoices_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_invoices"
ADD CONSTRAINT "billing_invoices_companyId_fkey"
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_invoices"
ADD CONSTRAINT "billing_invoices_subscriptionId_fkey"
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "subscription_invoices"
ADD CONSTRAINT "subscription_invoices_billingInvoiceId_fkey"
FOREIGN KEY ("billingInvoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_invoice_line_items"
ADD CONSTRAINT "billing_invoice_line_items_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_intents"
ADD CONSTRAINT "billing_payment_intents_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_intents"
ADD CONSTRAINT "billing_payment_intents_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_intents"
ADD CONSTRAINT "billing_payment_intents_paymentMethodId_fkey"
FOREIGN KEY ("paymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_payment_attempts"
ADD CONSTRAINT "billing_payment_attempts_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_attempts"
ADD CONSTRAINT "billing_payment_attempts_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_payment_attempts"
ADD CONSTRAINT "billing_payment_attempts_paymentIntentId_fkey"
FOREIGN KEY ("paymentIntentId") REFERENCES "billing_payment_intents"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_payment_attempts"
ADD CONSTRAINT "billing_payment_attempts_paymentMethodId_fkey"
FOREIGN KEY ("paymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_credit_balances"
ADD CONSTRAINT "billing_credit_balances_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_credit_ledger_entries"
ADD CONSTRAINT "billing_credit_ledger_entries_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_credit_ledger_entries"
ADD CONSTRAINT "billing_credit_ledger_entries_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_credit_notes"
ADD CONSTRAINT "billing_credit_notes_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_credit_notes"
ADD CONSTRAINT "billing_credit_notes_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_refunds"
ADD CONSTRAINT "billing_refunds_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_refunds"
ADD CONSTRAINT "billing_refunds_paymentAttemptId_fkey"
FOREIGN KEY ("paymentAttemptId") REFERENCES "billing_payment_attempts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_refunds"
ADD CONSTRAINT "billing_refunds_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_tax_records"
ADD CONSTRAINT "billing_tax_records_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "billing_events"
ADD CONSTRAINT "billing_events_billingAccountId_fkey"
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_events"
ADD CONSTRAINT "billing_events_invoiceId_fkey"
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_events"
ADD CONSTRAINT "billing_events_subscriptionId_fkey"
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "billing_events"
ADD CONSTRAINT "billing_events_companyId_fkey"
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;
INSERT INTO "billing_accounts" (
"id",
"companyId",
"isPrimary",
"legalName",
"billingEmail",
"billingAddress",
"taxExempt",
"defaultCurrency",
"invoiceTerms",
"netTermsDays",
"metadata"
)
SELECT
'ba_' || c."id",
c."id",
true,
c."name",
c."email",
c."address",
false,
COALESCE(a."currency", 'MAD'),
'DUE_ON_RECEIPT'::"BillingInvoiceTerms",
0,
'{}'::jsonb
FROM "companies" c
LEFT JOIN "accounting_settings" a ON a."companyId" = c."id"
WHERE NOT EXISTS (
SELECT 1
FROM "billing_accounts" ba
WHERE ba."companyId" = c."id" AND ba."isPrimary" = true
);
+408
View File
@@ -52,6 +52,99 @@ enum InvoiceStatus {
VOIDED
}
enum BillingInvoiceTerms {
DUE_ON_RECEIPT
NET_7
NET_15
NET_30
NET_45
NET_60
}
enum BillingInvoiceStatus {
DRAFT
OPEN
PAID
PARTIALLY_PAID
PAYMENT_PENDING
PAST_DUE
VOID
UNCOLLECTIBLE
REFUNDED
PARTIALLY_REFUNDED
}
enum BillingInvoiceType {
SUBSCRIPTION_INITIAL
SUBSCRIPTION_RENEWAL
TRIAL_CONVERSION
SUBSCRIPTION_UPGRADE
SUBSCRIPTION_DOWNGRADE_CREDIT
USAGE_BASED
ONE_TIME
MANUAL
CORRECTION
CANCELLATION_FEE
}
enum BillingLineItemType {
SUBSCRIPTION_FEE
SEAT_FEE
USAGE_FEE
SETUP_FEE
DISCOUNT
TAX
CREDIT
PRORATION
REFUND_ADJUSTMENT
MANUAL_ADJUSTMENT
PROFESSIONAL_SERVICES
CANCELLATION_FEE
}
enum BillingPaymentMethodType {
CARD
ACH_DEBIT
BANK_TRANSFER
WIRE_TRANSFER
MANUAL_INVOICE
PURCHASE_ORDER
}
enum BillingPaymentIntentStatus {
REQUIRES_PAYMENT_METHOD
REQUIRES_CONFIRMATION
REQUIRES_ACTION
PROCESSING
SUCCEEDED
FAILED
CANCELED
REFUNDED
PARTIALLY_REFUNDED
}
enum BillingPaymentAttemptStatus {
PENDING
PROCESSING
SUCCEEDED
FAILED
CANCELED
REFUNDED
PARTIALLY_REFUNDED
}
enum BillingRefundStatus {
PENDING
SUCCEEDED
FAILED
}
enum BillingCreditNoteStatus {
ISSUED
APPLIED
VOID
}
enum PaymentProvider {
AMANPAY
PAYPAL
@@ -341,6 +434,8 @@ model Company {
apiKey String @unique @default(cuid())
subscription Subscription?
billingAccounts BillingAccount[]
billingInvoices BillingInvoice[]
brand BrandSettings?
employees Employee[]
vehicles Vehicle[]
@@ -357,6 +452,7 @@ model Company {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
BillingEvent BillingEvent[]
@@map("companies")
}
@@ -384,10 +480,12 @@ model Subscription {
retryCount Int @default(0)
maxRetryCount Int @default(5)
invoices SubscriptionInvoice[]
billingInvoices BillingInvoice[]
events SubscriptionEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
BillingEvent BillingEvent[]
@@map("subscriptions")
}
@@ -421,6 +519,8 @@ model SubscriptionInvoice {
amanpayTransactionId String? @unique
paypalCaptureId String? @unique
paymentProvider PaymentProvider @default(AMANPAY)
billingInvoiceId String? @unique
billingInvoice BillingInvoice? @relation(fields: [billingInvoiceId], references: [id])
dueAt DateTime?
paidAt DateTime?
failedAt DateTime?
@@ -433,6 +533,314 @@ model SubscriptionInvoice {
@@map("subscription_invoices")
}
model BillingAccount {
id String @id @default(cuid())
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
isPrimary Boolean @default(true)
legalName String
billingEmail String
billingAddress Json?
taxId String?
taxExempt Boolean @default(false)
defaultCurrency String @default("MAD")
defaultPaymentMethodId String?
defaultPaymentMethod BillingPaymentMethod? @relation("BillingAccountDefaultPaymentMethod", fields: [defaultPaymentMethodId], references: [id])
invoiceTerms BillingInvoiceTerms @default(DUE_ON_RECEIPT)
netTermsDays Int @default(0)
providerCustomerId String?
dunningPaused Boolean @default(false)
dunningPausedAt DateTime?
dunningPausedBy String?
metadata Json @default("{}")
paymentMethods BillingPaymentMethod[] @relation("BillingAccountPaymentMethods")
invoices BillingInvoice[]
paymentIntents BillingPaymentIntent[]
paymentAttempts BillingPaymentAttempt[]
creditBalances BillingCreditBalance[]
creditLedgerEntries BillingCreditLedgerEntry[]
creditNotes BillingCreditNote[]
refunds BillingRefund[]
events BillingEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([companyId])
@@index([companyId, isPrimary])
@@map("billing_accounts")
}
model BillingPaymentMethod {
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation("BillingAccountPaymentMethods", fields: [billingAccountId], references: [id], onDelete: Cascade)
type BillingPaymentMethodType
providerPaymentMethodId String?
label String?
brand String?
last4 String?
expMonth Int?
expYear Int?
isDefault Boolean @default(false)
isActive Boolean @default(true)
metadata Json @default("{}")
defaultForBillingAccounts BillingAccount[] @relation("BillingAccountDefaultPaymentMethod")
paymentIntents BillingPaymentIntent[]
paymentAttempts BillingPaymentAttempt[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([billingAccountId])
@@map("billing_payment_methods")
}
model BillingInvoice {
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
subscriptionId String?
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
legacySubscriptionInvoice SubscriptionInvoice?
invoiceNumber String? @unique
invoiceSequence Int? @unique
invoiceType BillingInvoiceType
status BillingInvoiceStatus @default(DRAFT)
currency String @default("MAD")
subtotalAmount Int @default(0)
discountAmount Int @default(0)
creditAmount Int @default(0)
taxAmount Int @default(0)
totalAmount Int @default(0)
amountPaid Int @default(0)
amountDue Int @default(0)
invoiceDate DateTime?
dueAt DateTime?
finalizedAt DateTime?
paidAt DateTime?
voidedAt DateTime?
markedUncollectibleAt DateTime?
billingName String?
billingEmail String?
billingAddress Json?
providerInvoiceId String?
paymentProvider PaymentProvider?
isSubscriptionBlocking Boolean @default(false)
adminReason String?
metadata Json @default("{}")
createdByAdminId String?
lineItems BillingInvoiceLineItem[]
paymentIntents BillingPaymentIntent[]
paymentAttempts BillingPaymentAttempt[]
taxRecords BillingTaxRecord[]
creditNotes BillingCreditNote[]
refunds BillingRefund[]
events BillingEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
BillingCreditLedgerEntry BillingCreditLedgerEntry[]
@@index([billingAccountId])
@@index([companyId])
@@index([subscriptionId])
@@map("billing_invoices")
}
model BillingInvoiceLineItem {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
subscriptionId String?
plan Plan?
type BillingLineItemType
description String
quantity Int @default(1)
unitAmount Int
amount Int
currency String @default("MAD")
periodStart DateTime?
periodEnd DateTime?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([invoiceId])
@@map("billing_invoice_line_items")
}
model BillingPaymentIntent {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
paymentMethodId String?
paymentMethod BillingPaymentMethod? @relation(fields: [paymentMethodId], references: [id])
providerPaymentIntentId String?
status BillingPaymentIntentStatus @default(REQUIRES_PAYMENT_METHOD)
amount Int
currency String @default("MAD")
metadata Json @default("{}")
paymentAttempts BillingPaymentAttempt[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([invoiceId])
@@index([billingAccountId])
@@map("billing_payment_intents")
}
model BillingPaymentAttempt {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
paymentIntentId String?
paymentIntent BillingPaymentIntent? @relation(fields: [paymentIntentId], references: [id])
paymentMethodId String?
paymentMethod BillingPaymentMethod? @relation(fields: [paymentMethodId], references: [id])
providerPaymentId String?
status BillingPaymentAttemptStatus
amount Int
currency String @default("MAD")
failureCode String?
failureMessage String?
attemptedAt DateTime
metadata Json @default("{}")
refunds BillingRefund[]
createdAt DateTime @default(now())
@@index([invoiceId])
@@index([billingAccountId])
@@map("billing_payment_attempts")
}
model BillingCreditBalance {
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
currency String @default("MAD")
balanceAmount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([billingAccountId, currency])
@@map("billing_credit_balances")
}
model BillingCreditLedgerEntry {
id String @id @default(cuid())
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
invoiceId String?
invoice BillingInvoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
type String
amount Int
currency String @default("MAD")
reason String
createdBy String?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([billingAccountId])
@@map("billing_credit_ledger_entries")
}
model BillingCreditNote {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
amount Int
currency String @default("MAD")
reason String
status BillingCreditNoteStatus @default(ISSUED)
createdBy String?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([invoiceId])
@@index([billingAccountId])
@@map("billing_credit_notes")
}
model BillingRefund {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
paymentAttemptId String?
paymentAttempt BillingPaymentAttempt? @relation(fields: [paymentAttemptId], references: [id], onDelete: SetNull)
billingAccountId String
billingAccount BillingAccount @relation(fields: [billingAccountId], references: [id], onDelete: Cascade)
amount Int
currency String @default("MAD")
reason String
status BillingRefundStatus @default(PENDING)
providerRefundId String?
createdBy String?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([invoiceId])
@@index([billingAccountId])
@@map("billing_refunds")
}
model BillingTaxRecord {
id String @id @default(cuid())
invoiceId String
invoice BillingInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
jurisdiction String?
taxRate Float?
taxAmount Int
taxType String?
taxExempt Boolean @default(false)
exemptionReason String?
providerTaxId String?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([invoiceId])
@@map("billing_tax_records")
}
model BillingEvent {
id String @id @default(cuid())
billingAccountId String?
billingAccount BillingAccount? @relation(fields: [billingAccountId], references: [id], onDelete: SetNull)
invoiceId String?
invoice BillingInvoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
subscriptionId String?
subscription Subscription? @relation(fields: [subscriptionId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
eventType String
source String
payload Json @default("{}")
occurredAt DateTime
createdAt DateTime @default(now())
@@index([billingAccountId])
@@index([invoiceId])
@@index([subscriptionId])
@@index([companyId])
@@map("billing_events")
}
model PaymentAttempt {
id String @id @default(cuid())
invoiceId String