-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3df1e4a
Showing
16 changed files
with
1,914 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__pycache__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
""" | ||
) |
Oops, something went wrong.