Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dni committed Feb 17, 2023
0 parents commit 3df1e4a
Show file tree
Hide file tree
Showing 16 changed files with 1,914 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: release github version
on:
push:
tags:
- "[0-9]+.[0-9]+"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Invoices

## Create invoices that you can send to your client to pay online over Lightning.

This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.

## Usage

1. Create an invoice by clicking "NEW INVOICE"\
![create new invoice](https://imgur.com/a/Dce3wrr.png)
2. Fill the options for your INVOICE
- select the wallet
- select the fiat currency the invoice will be denominated in
- select a status for the invoice (default is draft)
- enter a company name, first name, last name, email, phone & address (optional)
- add one or more line items
- enter a name & price for each line item
3. You can then use share your invoice link with your customer to receive payment\
![invoice link](https://imgur.com/a/L0JOj4T.png)
36 changes: 36 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import asyncio

from fastapi import APIRouter
from starlette.staticfiles import StaticFiles

from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart

db = Database("ext_invoices")

invoices_static_files = [
{
"path": "/invoices/static",
"app": StaticFiles(directory="lnbits/extensions/invoices/static"),
"name": "invoices_static",
}
]

invoices_ext: APIRouter = APIRouter(prefix="/invoices", tags=["invoices"])


def invoices_renderer():
return template_renderer(["lnbits/extensions/invoices/templates"])


from .tasks import wait_for_paid_invoices


def invoices_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))


from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
6 changes: 6 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Invoices",
"short_description": "Create invoices for your clients.",
"tile": "/invoices/static/image/invoices.png",
"contributors": ["leesalminen"]
}
210 changes: 210 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
from typing import List, Optional, Union

from lnbits.helpers import urlsafe_short_hash

from . import db
from .models import (
CreateInvoiceData,
CreateInvoiceItemData,
Invoice,
InvoiceItem,
Payment,
UpdateInvoiceData,
UpdateInvoiceItemData,
)


async def get_invoice(invoice_id: str) -> Optional[Invoice]:
row = await db.fetchone(
"SELECT * FROM invoices.invoices WHERE id = ?", (invoice_id,)
)
return Invoice.from_row(row) if row else None


async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
rows = await db.fetchall(
"SELECT * FROM invoices.invoice_items WHERE invoice_id = ?", (invoice_id,)
)

return [InvoiceItem.from_row(row) for row in rows]


async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
row = await db.fetchone(
"SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
)
return InvoiceItem.from_row(row) if row else None


async def get_invoice_total(items: List[InvoiceItem]) -> int:
return sum(item.amount for item in items)


async def get_invoices(wallet_ids: Union[str, List[str]]) -> List[Invoice]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]

q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM invoices.invoices WHERE wallet IN ({q})", (*wallet_ids,)
)

return [Invoice.from_row(row) for row in rows]


async def get_invoice_payments(invoice_id: str) -> List[Payment]:
rows = await db.fetchall(
"SELECT * FROM invoices.payments WHERE invoice_id = ?", (invoice_id,)
)

return [Payment.from_row(row) for row in rows]


async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
row = await db.fetchone(
"SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
)
return Payment.from_row(row) if row else None


async def get_payments_total(payments: List[Payment]) -> int:
return sum(item.amount for item in payments)


async def create_invoice_internal(wallet_id: str, data: CreateInvoiceData) -> Invoice:
invoice_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO invoices.invoices (id, wallet, status, currency, company_name, first_name, last_name, email, phone, address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
invoice_id,
wallet_id,
data.status,
data.currency,
data.company_name,
data.first_name,
data.last_name,
data.email,
data.phone,
data.address,
),
)

invoice = await get_invoice(invoice_id)
assert invoice, "Newly created invoice couldn't be retrieved"
return invoice


async def create_invoice_items(
invoice_id: str, data: List[CreateInvoiceItemData]
) -> List[InvoiceItem]:
for item in data:
item_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO invoices.invoice_items (id, invoice_id, description, amount)
VALUES (?, ?, ?, ?)
""",
(
item_id,
invoice_id,
item.description,
int(item.amount * 100),
),
)

invoice_items = await get_invoice_items(invoice_id)
return invoice_items


async def update_invoice_internal(
wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
) -> Invoice:
await db.execute(
"""
UPDATE invoices.invoices
SET wallet = ?, currency = ?, status = ?, company_name = ?, first_name = ?, last_name = ?, email = ?, phone = ?, address = ?
WHERE id = ?
""",
(
wallet_id,
data.currency,
data.status,
data.company_name,
data.first_name,
data.last_name,
data.email,
data.phone,
data.address,
data.id,
),
)

invoice = await get_invoice(data.id)
assert invoice, "Newly updated invoice couldn't be retrieved"
return invoice


async def update_invoice_items(
invoice_id: str, data: List[UpdateInvoiceItemData]
) -> List[InvoiceItem]:
updated_items = []
for item in data:
if item.id:
updated_items.append(item.id)
await db.execute(
"""
UPDATE invoices.invoice_items
SET description = ?, amount = ?
WHERE id = ?
""",
(item.description, int(item.amount * 100), item.id),
)

placeholders = ",".join("?" for _ in range(len(updated_items)))
if not placeholders:
placeholders = "?"
updated_items = ["skip"]

await db.execute(
f"""
DELETE FROM invoices.invoice_items
WHERE invoice_id = ?
AND id NOT IN ({placeholders})
""",
(
invoice_id,
*tuple(updated_items),
),
)

for item in data:
if not item:
await create_invoice_items(
invoice_id=invoice_id,
data=[CreateInvoiceItemData(description=item.description)],
)

invoice_items = await get_invoice_items(invoice_id)
return invoice_items


async def create_invoice_payment(invoice_id: str, amount: int) -> Payment:
payment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO invoices.payments (id, invoice_id, amount)
VALUES (?, ?, ?)
""",
(
payment_id,
invoice_id,
amount,
),
)

payment = await get_invoice_payment(payment_id)
assert payment, "Newly created payment couldn't be retrieved"
return payment
55 changes: 55 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
async def m001_initial_invoices(db):

# STATUS COLUMN OPTIONS: 'draft', 'open', 'paid', 'canceled'

await db.execute(
f"""
CREATE TABLE invoices.invoices (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
currency TEXT NOT NULL,
company_name TEXT DEFAULT NULL,
first_name TEXT DEFAULT NULL,
last_name TEXT DEFAULT NULL,
email TEXT DEFAULT NULL,
phone TEXT DEFAULT NULL,
address TEXT DEFAULT NULL,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)

await db.execute(
f"""
CREATE TABLE invoices.invoice_items (
id TEXT PRIMARY KEY,
invoice_id TEXT NOT NULL,
description TEXT NOT NULL,
amount INTEGER NOT NULL,
FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
);
"""
)

await db.execute(
f"""
CREATE TABLE invoices.payments (
id TEXT PRIMARY KEY,
invoice_id TEXT NOT NULL,
amount {db.big_int} NOT NULL,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
);
"""
)
Loading

0 comments on commit 3df1e4a

Please sign in to comment.