{"id":28242136,"url":"https://github.com/frostwillmott/fund_raising","last_synced_at":"2026-07-04T17:31:10.170Z","repository":{"id":288749252,"uuid":"969068832","full_name":"FrostWillmott/fund_raising","owner":"FrostWillmott","description":"Django REST API: atomic payments with F() expressions, Redis cache  invalidation, Celery async email, custom permission inheritance,  CI coverage gates","archived":false,"fork":false,"pushed_at":"2026-07-03T12:18:50.000Z","size":512,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-03T14:19:50.832Z","etag":null,"topics":["celery","django","jwt","mailhog","mysql","redis","swagger"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FrostWillmott.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"AUDIT.md","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-19T10:08:50.000Z","updated_at":"2026-07-03T12:18:54.000Z","dependencies_parsed_at":"2025-04-19T16:12:23.982Z","dependency_job_id":"f0493777-80a0-4145-a4c3-2e367cdd70a5","html_url":"https://github.com/FrostWillmott/fund_raising","commit_stats":null,"previous_names":["frostwillmott/fund_raising"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/FrostWillmott/fund_raising","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrostWillmott%2Ffund_raising","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrostWillmott%2Ffund_raising/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrostWillmott%2Ffund_raising/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrostWillmott%2Ffund_raising/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FrostWillmott","download_url":"https://codeload.github.com/FrostWillmott/fund_raising/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrostWillmott%2Ffund_raising/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35130722,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["celery","django","jwt","mailhog","mysql","redis","swagger"],"created_at":"2025-05-19T05:09:29.310Z","updated_at":"2026-07-04T17:31:10.164Z","avatar_url":"https://github.com/FrostWillmott.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\".github/banner.svg\" alt=\"Group Fundraising Service\" width=\"100%\"/\u003e\n\u003c/div\u003e\n\n# Group Fundraising Service\n\n[![CI](https://github.com/FrostWillmott/fund_raising/actions/workflows/ci.yml/badge.svg)](https://github.com/FrostWillmott/fund_raising/actions/workflows/ci.yml)\n[![Coverage](https://raw.githubusercontent.com/FrostWillmott/fund_raising/main/.github/badges/coverage.svg)](https://github.com/FrostWillmott/fund_raising/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/badge/python-3.12-blue?logo=python\u0026logoColor=white)](https://www.python.org/)\n[![Django](https://img.shields.io/badge/django-5.2-092E20?logo=django\u0026logoColor=white)](https://www.djangoproject.com/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker\u0026logoColor=white)](https://www.docker.com/)\n\nA production-ready REST API for group fundraising with JWT authentication,\nimage processing, cache invalidation, and asynchronous email notifications.\nThe project focuses on practical backend concerns: transactional safety,\npermissions, query optimization, and maintainable environment-based settings.\n\n## What's inside\n\n**Core stack:** Django 5.2, Django REST Framework, MySQL, Redis, Celery\n\n**Features:**\n- JWT authentication via djoser + simplejwt (not mentioned in the spec)\n- Separate dev/prod settings with environment-specific email, DB, and debug config\n- Service layer (`collects/services.py`, `payments/services.py`): creation\n  lifecycle — transaction, forced payment status, atomic `collected_amount`\n  increment via `F()` expressions, post-commit email and cache invalidation —\n  lives in one place per domain\n- Domain validation where money moves: positive amounts only, no donations\n  to inactive or expired collects, `end_date` must be after `start_date`\n- Race-safe `transaction_id` uniqueness: serializer check for a clear error\n  message, plus `IntegrityError` → 400 for concurrent duplicates\n- Collect list/detail responses cached for 60 s with working invalidation:\n  `cache_page(key_prefix=...)` puts a matchable literal into the cache key,\n  `delete_pattern` removes it on every write; a Redis outage degrades to\n  uncached responses instead of 500s (`IGNORE_EXCEPTIONS`)\n- Payments are private to the payer: list/retrieve are payer-scoped and not\n  shared-cached\n- A collect with payment history cannot be deleted — friendly 400 at the API\n  layer, `on_delete=PROTECT` as the DB-level backstop\n- Celery email tasks with bounded retries (`autoretry_for` + exponential\n  backoff, max 3 attempts); a broker outage after commit is logged, not\n  turned into a 500\n- Cover image processing: auto-resize to 1200×800, JPEG optimization, format\n  validation, 2MB size limit, auto-cleanup of old files on update\n- Custom permission classes with inheritance (`IsOwnerOrReadOnly` →\n  `IsCollectAuthorOrReadOnly`, `IsPaymentPayerOrReadOnly`)\n- DB index on `(collect, status)` for payment queries\n- Pagination with configurable `page_size`\n- Realistic seed data: Faker with `ru_RU` locale, occasion-specific title/\n  description templates, weighted payment amount distribution, batch inserts\n- Poetry for dependency management, mypy + django-stubs for type checking,\n  ruff for linting\n- MailHog with MongoDB backend for email testing in dev\n\n## Quick start\n\nPrerequisites: Docker and Docker Compose.\n```bash\ngit clone https://github.com/FrostWillmott/fund_raising.git\ncd fund_raising\ncp .env.example .env\ndocker compose up --build\n```\n\n| Service | URL |\n|---|---|\n| API | http://localhost:8000/api/v1/ |\n| Swagger UI | http://localhost:8000/docs/ |\n| MailHog | http://localhost:8025/ |\n\n## Production deployment\n\nUse the production override to run Django via Gunicorn and force production settings:\n```bash\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build\n```\n\nRequired production env vars:\n- `SECRET_KEY`\n- `ALLOWED_HOSTS`\n- `CORS_ALLOWED_ORIGINS` (comma-separated origins)\n\n## API\n\nAuthentication: JWT Bearer token.\n```bash\n# Get token\ncurl -X POST http://localhost:8000/auth/jwt/create/ \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\": \"user\", \"password\": \"password\"}'\n```\n\nEndpoints:\n```\nPOST   /auth/users/                  — register\nPOST   /auth/jwt/create/             — get token\nPOST   /auth/jwt/refresh/            — refresh token\n\nGET    /api/v1/collects/             — list collections (no payment feed)\nPOST   /api/v1/collects/             — create collection (with cover image)\nGET    /api/v1/collects/{id}/        — collection detail with the 10 most recent payments\nPATCH  /api/v1/collects/{id}/        — update (owner only)\nDELETE /api/v1/collects/{id}/        — delete (owner only, no payments)\n\nGET    /api/v1/payments/             — list your own payments\nPOST   /api/v1/payments/             — make a donation\n```\n\nFull interactive docs at `/docs/`.\n\n## Seed data\n```bash\ndocker compose exec web python manage.py generate_test_data \\\n  --users 50 --collects 100 --payments 5000\n```\n\nGenerates realistic Russian-language collections (birthday, wedding,\nnew year, other) with occasion-specific titles and descriptions, and\npayments with weighted amount distribution (50% small / 30% medium /\n15% large / 5% whale).\n\n## Project structure\n```\nfund_raising/\n├── api/v1/\n│   ├── collects/     # ViewSet (cached, list/detail serializer split)\n│   └── payments/     # ViewSet (payer-scoped), serializers\n├── collects/         # Model, services, signals, image utils, tasks\n├── payments/         # Model, services, tasks\n├── dev_tools/        # generate_test_data management command\n└── fund_raising/     # Settings (base / development / production), cache helper\n```\n\n## Development\n```bash\n# Install git hooks (once per clone)\npoetry run pre-commit install --hook-type pre-commit --hook-type pre-push\n\n# Linting\ndocker compose exec web ruff check .\n\n# Type checking\ndocker compose exec web mypy .\n\n# Run hooks manually for all files\npoetry run pre-commit run --all-files\n```\n## Tests\n\n32 tests across the API and service layers:\n- collection listing/creation, including cache freshness (a created collect\n  appears in the list immediately) and a cache-key regression guard\n- permission and visibility enforcement: owner-only writes, payer-scoped\n  payment access (list and retrieve)\n- domain validation: negative/zero amounts, donations to inactive or expired\n  collects, `end_date` in the past, null/missing collect\n- duplicate `transaction_id` rejection and delete guards (API 400 + DB PROTECT)\n- query-count regression for the collect list (constant, independent of the\n  number of payments) and the bounded detail feed\n- cover image pipeline: size/format validation, resize, JPEG re-encoding\n- payment service unit tests: counter increment, failed duplicate leaves the\n  counter intact, direct ORM writes don't touch the counter\n\n```bash\n# Run with coverage (outputs term-missing summary + htmlcov/)\npytest\n\n# Quick run without coverage\npytest --no-cov\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrostwillmott%2Ffund_raising","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrostwillmott%2Ffund_raising","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrostwillmott%2Ffund_raising/lists"}